models.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676
  1. import logging
  2. from typing import Dict, Optional
  3. from uuid import uuid4
  4. import musicbrainzngs
  5. import requests
  6. from django.conf import settings
  7. from django.core.files.base import ContentFile
  8. from django.db import models
  9. from django.urls import reverse
  10. from django.utils.translation import gettext_lazy as _
  11. from django_extensions.db.models import TimeStampedModel
  12. from imagekit.models import ImageSpecField
  13. from imagekit.processors import ResizeToFit
  14. from music.allmusic import get_allmusic_slug, scrape_data_from_allmusic
  15. from music.bandcamp import get_bandcamp_slug
  16. from music.musicbrainz import (
  17. lookup_album_dict_from_mb,
  18. lookup_album_from_mb,
  19. lookup_track_from_mb,
  20. lookup_artist_from_mb,
  21. )
  22. from music.theaudiodb import lookup_album_from_tadb, lookup_artist_from_tadb
  23. from music.utils import clean_artist_name
  24. from scrobbles.mixins import ScrobblableConstants, ScrobblableMixin
  25. logger = logging.getLogger(__name__)
  26. BNULL = {"blank": True, "null": True}
  27. class Artist(TimeStampedModel):
  28. """Represents a music artist.
  29. # Lookup or create by title alone
  30. >>> Artist.find_or_create(name="Bon Iver")
  31. # Lookup or create by MB id alone
  32. >>> Artist.find_or_create(musicbrainz_id="0307edfc-437c-4b48-8700-80680e66a228")
  33. """
  34. uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
  35. name = models.CharField(max_length=255)
  36. biography = models.TextField(**BNULL)
  37. theaudiodb_id = models.CharField(max_length=255, unique=True, **BNULL)
  38. theaudiodb_genre = models.CharField(max_length=255, **BNULL)
  39. theaudiodb_mood = models.CharField(max_length=255, **BNULL)
  40. musicbrainz_id = models.CharField(max_length=255, **BNULL)
  41. allmusic_id = models.CharField(max_length=100, **BNULL)
  42. bandcamp_id = models.CharField(max_length=100, **BNULL)
  43. thumbnail = models.ImageField(upload_to="artist/", **BNULL)
  44. thumbnail_small = ImageSpecField(
  45. source="thumbnail",
  46. processors=[ResizeToFit(100, 100)],
  47. format="JPEG",
  48. options={"quality": 60},
  49. )
  50. thumbnail_medium = ImageSpecField(
  51. source="thumbnail",
  52. processors=[ResizeToFit(300, 300)],
  53. format="JPEG",
  54. options={"quality": 75},
  55. )
  56. alt_names = models.TextField(**BNULL)
  57. class Meta:
  58. unique_together = [["name", "musicbrainz_id"]]
  59. def __str__(self):
  60. return self.name
  61. @property
  62. def primary_image_url(self) -> str:
  63. if self.thumbnail:
  64. return self.thumbnail.url
  65. if self.album_set.first().cover_image:
  66. return self.album_set.first().cover_image.url
  67. return ""
  68. @property
  69. def mb_link(self) -> str:
  70. if self.musicbrainz_id:
  71. return f"https://musicbrainz.org/artist/{self.musicbrainz_id}"
  72. return ""
  73. @property
  74. def allmusic_link(self):
  75. if self.allmusic_id:
  76. return f"https://www.allmusic.com/artist/{self.allmusic_id}"
  77. return ""
  78. @property
  79. def bandcamp_link(self):
  80. if self.bandcamp_id:
  81. return f"https://{self.bandcamp_id}.bandcamp.com/"
  82. return ""
  83. def get_absolute_url(self):
  84. return reverse("music:artist_detail", kwargs={"slug": self.uuid})
  85. def scrobbles(self):
  86. from scrobbles.models import Scrobble
  87. return Scrobble.objects.filter(
  88. track__in=self.track_set.all()
  89. ).order_by("-timestamp")
  90. @property
  91. def tracks(self):
  92. return (
  93. self.track_set.all()
  94. .annotate(scrobble_count=models.Count("scrobble"))
  95. .order_by("-scrobble_count")
  96. )
  97. def charts(self):
  98. from scrobbles.models import ChartRecord
  99. return ChartRecord.objects.filter(track__artist=self).order_by("-year")
  100. def scrape_allmusic(self, force=False) -> None:
  101. if not self.allmusic_id or force:
  102. slug = get_allmusic_slug(self.name)
  103. if not slug:
  104. logger.info(
  105. "No allmusic link found", extra={"track_id": self.id}
  106. )
  107. return
  108. self.allmusic_id = slug
  109. self.save(update_fields=["allmusic_id"])
  110. def scrape_bandcamp(self, force=False) -> None:
  111. if not self.bandcamp_id or force:
  112. slug = get_bandcamp_slug(self.name)
  113. if not slug:
  114. logger.info(
  115. "No bandcamp link found", extra={"track_id": self.id}
  116. )
  117. return
  118. self.bandcamp_id = slug
  119. self.save(update_fields=["bandcamp_id"])
  120. def fix_metadata(self, force_update=False):
  121. tadb_info = {}
  122. if self.theaudiodb_id and force_update:
  123. tadb_info = lookup_artist_from_tadb(self.theaudiodb_id)
  124. if not self.theaudiodb_id or (force_update and not tadb_info):
  125. tadb_info = lookup_artist_from_tadb(self.name)
  126. if not tadb_info:
  127. logger.warn(
  128. f"No response from TADB for artist {self.name}, try force_update=True"
  129. )
  130. return
  131. self.biography = tadb_info["biography"]
  132. self.theaudiodb_genre = tadb_info["genre"]
  133. self.theaudiodb_mood = tadb_info["mood"]
  134. thumb_url = tadb_info.get("thumb_url", "")
  135. if thumb_url:
  136. r = requests.get(thumb_url)
  137. if r.status_code == 200:
  138. fname = f"{self.name}_{self.uuid}.jpg"
  139. self.thumbnail.save(fname, ContentFile(r.content), save=True)
  140. @property
  141. def rym_link(self):
  142. artist_slug = self.name.lower().replace(" ", "-").replace(",", "")
  143. return f"https://rateyourmusic.com/artist/{artist_slug}/"
  144. @property
  145. def bandcamp_search_link(self):
  146. artist = self.name.lower()
  147. return f"https://bandcamp.com/search?q={artist}&item_type=b"
  148. @classmethod
  149. def find_or_create(
  150. cls, name: str = "", musicbrainz_id: str = ""
  151. ) -> "Artist":
  152. keys = {}
  153. if name:
  154. name = clean_artist_name(name)
  155. keys["name"] = name
  156. if musicbrainz_id:
  157. keys["musicbrainz_id"] = musicbrainz_id
  158. if not keys:
  159. raise Exception("Must have name, mb_id or both to lookup artist")
  160. artist = cls.objects.filter(**keys).first()
  161. if not artist:
  162. artist = cls.objects.filter(
  163. models.Q(name=name) | models.Q(alt_names__icontains=name)
  164. ).first()
  165. # Does not exist, look it up from Musicbrainz
  166. if not artist:
  167. alt_name = None
  168. try:
  169. artist_dict = lookup_artist_from_mb(name)
  170. musicbrainz_id = musicbrainz_id or artist_dict.get("id", "")
  171. if name != artist_dict.get("name", ""):
  172. alt_name = name
  173. name = artist_dict.get("name", "")
  174. except ValueError:
  175. pass
  176. if musicbrainz_id:
  177. artist = cls.objects.filter(
  178. musicbrainz_id=musicbrainz_id
  179. ).first()
  180. if artist and alt_name:
  181. if not artist.alt_names:
  182. artist.alt_names = alt_name
  183. else:
  184. artist.alt_names += f"\\{alt_name}"
  185. artist.save(update_fields=["alt_names"])
  186. if not artist:
  187. artist = cls.objects.create(
  188. name=name, musicbrainz_id=musicbrainz_id, alt_names=alt_name
  189. )
  190. # TODO maybe this should be spun off into an async task?
  191. artist.fix_metadata()
  192. return artist
  193. class Album(TimeStampedModel):
  194. uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
  195. name = models.CharField(max_length=255)
  196. album_artist = models.ForeignKey(
  197. Artist, related_name="albums", on_delete=models.DO_NOTHING, **BNULL
  198. )
  199. artists = models.ManyToManyField(Artist)
  200. year = models.IntegerField(**BNULL)
  201. musicbrainz_id = models.CharField(max_length=255, unique=True, **BNULL)
  202. musicbrainz_releasegroup_id = models.CharField(max_length=255, **BNULL)
  203. musicbrainz_albumartist_id = models.CharField(max_length=255, **BNULL)
  204. cover_image = models.ImageField(upload_to="albums/", **BNULL)
  205. cover_image_small = ImageSpecField(
  206. source="cover_image",
  207. processors=[ResizeToFit(100, 100)],
  208. format="JPEG",
  209. options={"quality": 60},
  210. )
  211. cover_image_medium = ImageSpecField(
  212. source="cover_image",
  213. processors=[ResizeToFit(300, 300)],
  214. format="JPEG",
  215. options={"quality": 75},
  216. )
  217. theaudiodb_id = models.CharField(max_length=255, unique=True, **BNULL)
  218. theaudiodb_description = models.TextField(**BNULL)
  219. theaudiodb_year_released = models.IntegerField(**BNULL)
  220. theaudiodb_score = models.FloatField(**BNULL)
  221. theaudiodb_score_votes = models.IntegerField(**BNULL)
  222. theaudiodb_genre = models.CharField(max_length=255, **BNULL)
  223. theaudiodb_style = models.CharField(max_length=255, **BNULL)
  224. theaudiodb_mood = models.CharField(max_length=255, **BNULL)
  225. theaudiodb_speed = models.CharField(max_length=255, **BNULL)
  226. theaudiodb_theme = models.CharField(max_length=255, **BNULL)
  227. allmusic_id = models.CharField(max_length=255, **BNULL)
  228. allmusic_rating = models.IntegerField(**BNULL)
  229. allmusic_review = models.TextField(**BNULL)
  230. bandcamp_id = models.CharField(max_length=100, **BNULL)
  231. rateyourmusic_id = models.CharField(max_length=255, **BNULL)
  232. wikipedia_slug = models.CharField(max_length=255, **BNULL)
  233. discogs_id = models.CharField(max_length=255, **BNULL)
  234. wikidata_id = models.CharField(max_length=255, **BNULL)
  235. alt_names = models.TextField(**BNULL)
  236. def __str__(self) -> str:
  237. return "{} by {}".format(self.name, self.album_artist)
  238. def get_absolute_url(self):
  239. return reverse("music:album_detail", kwargs={"slug": self.uuid})
  240. def scrobbles(self):
  241. from scrobbles.models import Scrobble
  242. return Scrobble.objects.filter(
  243. track__in=self.track_set.all()
  244. ).order_by("-timestamp")
  245. @property
  246. def primary_image_url(self) -> str:
  247. if self.cover_image:
  248. return self.cover_image_medium.url
  249. return ""
  250. @property
  251. def tracks(self):
  252. return (
  253. self.track_set.all()
  254. .annotate(scrobble_count=models.Count("scrobble"))
  255. .order_by("-scrobble_count")
  256. )
  257. def fix_album_artist(self):
  258. from music.utils import get_or_create_various_artists
  259. multiple_artists = self.artists.count() > 1
  260. if multiple_artists:
  261. self.album_artist = get_or_create_various_artists()
  262. else:
  263. self.album_artist = self.artists.first()
  264. self.save(update_fields=["album_artist"])
  265. def scrape_allmusic(self, force=False) -> None:
  266. if not self.name:
  267. logger.warning(
  268. "Album without a name cannot be scraped",
  269. extra={"album_id": self.id},
  270. )
  271. return
  272. if not self.allmusic_id or force:
  273. slug = get_allmusic_slug(self.album_artist.name, self.name)
  274. if not slug:
  275. logger.info(
  276. f"No allmsuic link for {self} by {self.album_artist}"
  277. )
  278. return
  279. self.allmusic_id = slug
  280. self.save(update_fields=["allmusic_id"])
  281. allmusic_data = None
  282. if self.allmusic_link:
  283. allmusic_data = scrape_data_from_allmusic(self.allmusic_link)
  284. if not allmusic_data:
  285. logger.info(f"No allmsuic data for {self} by {self.album_artist}")
  286. return
  287. self.allmusic_review = allmusic_data["review"]
  288. self.allmusic_rating = allmusic_data["rating"]
  289. self.save(update_fields=["allmusic_review", "allmusic_rating"])
  290. def scrape_theaudiodb(self) -> None:
  291. artist = "Various Artists"
  292. if self.album_artist:
  293. artist = self.album_artist.name
  294. album_data = lookup_album_from_tadb(self.name, artist)
  295. if not album_data.get("theaudiodb_id"):
  296. logger.info(f"No data for {self} found in TheAudioDB")
  297. return
  298. Album.objects.filter(pk=self.pk).update(**album_data)
  299. def scrape_bandcamp(self, force=False) -> None:
  300. if not self.bandcamp_id or force:
  301. slug = get_bandcamp_slug(self.album_artist.name, self.name)
  302. if not slug:
  303. logger.info(f"No bandcamp link for {self}")
  304. return
  305. self.bandcamp_id = slug
  306. self.save(update_fields=["bandcamp_id"])
  307. def fix_metadata(self):
  308. if (
  309. not self.musicbrainz_albumartist_id
  310. or not self.year
  311. or not self.musicbrainz_releasegroup_id
  312. ):
  313. musicbrainzngs.set_useragent("vrobbler", "0.3.0")
  314. mb_data = musicbrainzngs.get_release_by_id(
  315. self.musicbrainz_id, includes=["artists", "release-groups"]
  316. )
  317. if not self.musicbrainz_releasegroup_id:
  318. self.musicbrainz_releasegroup_id = mb_data["release"][
  319. "release-group"
  320. ]["id"]
  321. if not self.musicbrainz_albumartist_id:
  322. self.musicbrainz_albumartist_id = mb_data["release"][
  323. "artist-credit"
  324. ][0]["artist"]["id"]
  325. if not self.year:
  326. try:
  327. self.year = mb_data["release"]["date"][0:4]
  328. except KeyError:
  329. pass
  330. except IndexError:
  331. pass
  332. self.save(
  333. update_fields=[
  334. "musicbrainz_albumartist_id",
  335. "musicbrainz_releasegroup_id",
  336. "year",
  337. ]
  338. )
  339. new_artist = Artist.objects.filter(
  340. musicbrainz_id=self.musicbrainz_albumartist_id
  341. ).first()
  342. if self.musicbrainz_albumartist_id and new_artist:
  343. self.artists.add(new_artist)
  344. if not new_artist:
  345. for t in self.track_set.all():
  346. self.artists.add(t.artist)
  347. if (
  348. not self.cover_image
  349. or self.cover_image == "default-image-replace-me"
  350. ):
  351. self.fetch_artwork()
  352. self.fix_album_artist()
  353. self.scrape_theaudiodb()
  354. self.scrape_allmusic()
  355. def fetch_artwork(self, force=False):
  356. if not self.cover_image and not force:
  357. if self.musicbrainz_id:
  358. try:
  359. img_data = musicbrainzngs.get_image_front(
  360. self.musicbrainz_id
  361. )
  362. name = f"{self.name}_{self.uuid}.jpg"
  363. self.cover_image = ContentFile(img_data, name=name)
  364. logger.info(f"Setting image to {name}")
  365. except musicbrainzngs.ResponseError:
  366. logger.warning(
  367. f"No cover art found for {self.name} by release"
  368. )
  369. if (
  370. not self.cover_image
  371. or self.cover_image == "default-image-replace-me"
  372. ) and self.musicbrainz_releasegroup_id:
  373. try:
  374. img_data = musicbrainzngs.get_release_group_image_front(
  375. self.musicbrainz_releasegroup_id
  376. )
  377. name = f"{self.name}_{self.uuid}.jpg"
  378. self.cover_image = ContentFile(img_data, name=name)
  379. logger.info(f"Setting image to {name}")
  380. except musicbrainzngs.ResponseError:
  381. logger.warning(
  382. f"No cover art found for {self.name} by release group"
  383. )
  384. if not self.cover_image:
  385. logger.debug(
  386. f"No cover art found for release or release group for {self.name}, setting to default"
  387. )
  388. self.save()
  389. @property
  390. def mb_link(self) -> str:
  391. return f"https://musicbrainz.org/release/{self.musicbrainz_id}"
  392. @property
  393. def allmusic_link(self) -> str:
  394. if self.allmusic_id:
  395. return f"https://www.allmusic.com/album/{self.allmusic_id}"
  396. return ""
  397. @property
  398. def wikipedia_link(self):
  399. if self.wikipedia_slug:
  400. return f"https://www.wikipedia.org/en/{self.wikipedia_slug}"
  401. return ""
  402. @property
  403. def tadb_link(self):
  404. if self.theaudiodb_id:
  405. return f"https://www.theaudiodb.com/album/{self.theaudiodb_id}"
  406. return ""
  407. @property
  408. def rym_link(self):
  409. artist_slug = self.album_artist.name.lower().replace(" ", "-")
  410. album_slug = self.name.lower().replace(" ", "-")
  411. return f"https://rateyourmusic.com/release/album/{artist_slug}/{album_slug}/"
  412. @property
  413. def bandcamp_link(self):
  414. if self.bandcamp_id and self.album_artist.bandcamp_id:
  415. return f"https://{self.album_artist.bandcamp_id}.bandcamp.com/album/{self.bandcamp_id}"
  416. return ""
  417. @property
  418. def bandcamp_search_link(self):
  419. artist = self.album_artist.name.lower()
  420. album = self.name.lower()
  421. return f"https://bandcamp.com/search?q={album} {artist}&item_type=a"
  422. @classmethod
  423. def find_or_create(
  424. cls, name: str, artist_name: str, musicbrainz_id: str = ""
  425. ) -> "Album":
  426. if not name or not artist_name:
  427. raise Exception(
  428. "Must have at least name and artist name to lookup album"
  429. )
  430. album = None
  431. if musicbrainz_id:
  432. album = cls.objects.filter(
  433. musicbrainz_id=musicbrainz_id,
  434. name=name,
  435. album_artist__name=artist_name,
  436. ).first()
  437. if not album and musicbrainz_id:
  438. album = cls.objects.filter(
  439. musicbrainz_id=musicbrainz_id,
  440. ).first()
  441. if not album:
  442. album = cls.objects.filter(
  443. models.Q(name=name) | models.Q(alt_names__icontains=name),
  444. album_artist__name=artist_name,
  445. ).first()
  446. if not album:
  447. alt_name = None
  448. try:
  449. album_dict = lookup_album_dict_from_mb(
  450. name, artist_name=artist_name
  451. )
  452. musicbrainz_id = musicbrainz_id or album_dict.get("mb_id", "")
  453. found_name = album_dict.get("title", "")
  454. if found_name and name != found_name:
  455. alt_name = name
  456. name = found_name
  457. except ValueError:
  458. pass
  459. if musicbrainz_id:
  460. album = cls.objects.filter(
  461. musicbrainz_id=musicbrainz_id
  462. ).first()
  463. if album and alt_name:
  464. if not album.alt_names:
  465. album.alt_names = alt_name
  466. else:
  467. album.alt_names += f"\\{alt_name}"
  468. album.save(update_fields=["alt_names"])
  469. if not album:
  470. artist = Artist.find_or_create(name=artist_name)
  471. album = cls.objects.create(
  472. name=name,
  473. album_artist=artist,
  474. musicbrainz_id=musicbrainz_id,
  475. alt_names=alt_name,
  476. )
  477. # TODO maybe do this in a separate process?
  478. album.fix_metadata()
  479. return album
  480. class Track(ScrobblableMixin):
  481. COMPLETION_PERCENT = getattr(settings, "MUSIC_COMPLETION_PERCENT", 100)
  482. artist = models.ForeignKey(Artist, on_delete=models.DO_NOTHING)
  483. albums = models.ManyToManyField(Album, related_name="tracks")
  484. album = models.ForeignKey(Album, on_delete=models.DO_NOTHING, **BNULL)
  485. musicbrainz_id = models.CharField(max_length=255, **BNULL)
  486. class Meta:
  487. unique_together = [["album", "musicbrainz_id"]]
  488. def __str__(self):
  489. return f"{self.title} by {self.artist}"
  490. @property
  491. def primary_album(self):
  492. return self.albums.order_by("year").first()
  493. def get_absolute_url(self):
  494. return reverse("music:track_detail", kwargs={"slug": self.uuid})
  495. @property
  496. def subtitle(self) -> str:
  497. return str(self.artist)
  498. @property
  499. def strings(self) -> ScrobblableConstants:
  500. return ScrobblableConstants(verb="Listening", tags="notes")
  501. @property
  502. def mb_link(self):
  503. return f"https://musicbrainz.org/recording/{self.musicbrainz_id}"
  504. @property
  505. def info_link(self):
  506. return self.mb_link
  507. @property
  508. def primary_image_url(self) -> str:
  509. url = ""
  510. if self.artist.thumbnail:
  511. url = self.artist.thumbnail_medium.url
  512. if self.album and self.album.cover_image:
  513. url = self.album.cover_image_medium.url
  514. return url
  515. @classmethod
  516. def find_or_create(
  517. cls,
  518. title: str = "",
  519. artist_name: str = "",
  520. musicbrainz_id: str = "",
  521. album_name: str = "",
  522. run_time_seconds: int = 900,
  523. enrich: bool = False,
  524. commit: bool = True,
  525. ) -> "Track":
  526. """Given a name, try to find the track by the artist from Musicbrainz.
  527. As a basic conceit we trust the source for giving us the track and artist
  528. name
  529. Optionally, we can update any found artists with overwrite."""
  530. created = False
  531. if musicbrainz_id:
  532. track = cls.objects.filter(musicbrainz_id=musicbrainz_id).first()
  533. artist = track.artist
  534. if not track and not (title and album_name):
  535. raise Exception(
  536. "Cannot find track with musicbrainz_id and no track title or artist name provided."
  537. )
  538. else:
  539. artist = Artist.find_or_create(artist_name)
  540. track, created = cls.objects.get_or_create(
  541. title=title, artist=artist
  542. )
  543. if not created:
  544. logger.info(
  545. "Found exact match for track by name and artist",
  546. extra={
  547. "title": title,
  548. "artist_name": artist_name,
  549. "track_id": track.id,
  550. },
  551. )
  552. if track.album and album_name != track.album.name:
  553. # TODO found track, but it's on a different album ... associations?
  554. logger.info("Found track by artist, but album is different.")
  555. album = Album.find_or_create()
  556. if enrich:
  557. album = None
  558. if album_name:
  559. album = Album.find_or_create(album_name)
  560. if artist.musicbrainz_id:
  561. track_dict = lookup_track_from_mb(title, artist.musicbrainz_id)
  562. musicbrainz_id = musicbrainz_id or track_dict.get("id", "")
  563. found_title: bool = track_dict.get("name", False)
  564. mismatched_title: bool = title != track_dict.get("name", "")
  565. if found_title and mismatched_title:
  566. logger.warning(
  567. "Source track title and found title do not match",
  568. extra={"title": title, "track_dict": track_dict},
  569. )
  570. if not run_time_seconds:
  571. run_time_seconds = int(
  572. int(track_dict.get("length", 900000)) / 1000
  573. )
  574. track.album = album
  575. track.artist = artist
  576. track.run_time_seconds = run_time_seconds
  577. if commit:
  578. track.save()
  579. # TODO Also set cover art and tags
  580. return track