diff --git a/config/settings/common.py b/config/settings/common.py
index 863954e2d1929edaafb7952c20e252a50415c51c..9ac3b47e4b321f9e1b996e882e419c589fd97ca8 100644
--- a/config/settings/common.py
+++ b/config/settings/common.py
@@ -119,7 +119,7 @@ MANAGERS = ADMINS
# See: https://docs.djangoproject.com/en/dev/ref/settings/#databases
DATABASES = {
# Raises ImproperlyConfigured exception if DATABASE_URL not in os.environ
- 'default': env.db("DATABASE_URL", default="postgres:///postgres"),
+ 'default': env.db("DATABASE_URL", default="postgresql://postgres@postgres/postgres"),
}
DATABASES['default']['ATOMIC_REQUESTS'] = True
#
diff --git a/docker/Dockerfile.local b/docker/Dockerfile.local
index 12760ea03ec02d9f2a8f07a1f9b83caca9ae2383..b70410459bebc0ed83d9fdd61d341c5b0b6bb26f 100644
--- a/docker/Dockerfile.local
+++ b/docker/Dockerfile.local
@@ -8,9 +8,5 @@ COPY ./install_os_dependencies.sh /install_os_dependencies.sh
RUN bash install_os_dependencies.sh install
COPY ./requirements /requirements
RUN pip install -r /requirements/local.txt
-COPY ./compose/django/entrypoint.sh /entrypoint.sh
-RUN chmod +x /entrypoint.sh
WORKDIR /app
-
-ENTRYPOINT ["/entrypoint.sh"]
diff --git a/funkwhale_api/music/admin.py b/funkwhale_api/music/admin.py
index b18d54032ca3b28840d80e1fe05a8d52639e0707..a6a7f94f3b90abae353bc85f2a51734db6a31edb 100644
--- a/funkwhale_api/music/admin.py
+++ b/funkwhale_api/music/admin.py
@@ -16,7 +16,7 @@ class AlbumAdmin(admin.ModelAdmin):
@admin.register(models.Track)
class TrackAdmin(admin.ModelAdmin):
list_display = ['title', 'artist', 'album', 'mbid']
- search_fields = ['title', 'artist__name', 'album__name', 'mbid']
+ search_fields = ['title', 'artist__name', 'album__title', 'mbid']
list_select_related = True
@admin.register(models.ImportBatch)
@@ -29,3 +29,19 @@ class ImportJobAdmin(admin.ModelAdmin):
list_select_related = True
search_fields = ['source', 'batch__pk', 'mbid']
list_filter = ['status']
+
+
+@admin.register(models.Work)
+class WorkAdmin(admin.ModelAdmin):
+ list_display = ['title', 'mbid', 'language', 'nature']
+ list_select_related = True
+ search_fields = ['title']
+ list_filter = ['language', 'nature']
+
+
+@admin.register(models.Lyrics)
+class LyricsAdmin(admin.ModelAdmin):
+ list_display = ['url', 'id', 'url']
+ list_select_related = True
+ search_fields = ['url', 'work__title']
+ list_filter = ['work__language']
diff --git a/funkwhale_api/music/importers.py b/funkwhale_api/music/importers.py
index 33131938c339d5de24a280dbdea416d40021ff2a..7e26fe9689571cccd86d888efc8a1eaaacacc3d5 100644
--- a/funkwhale_api/music/importers.py
+++ b/funkwhale_api/music/importers.py
@@ -38,4 +38,5 @@ registry = {
'Artist': Importer,
'Track': Importer,
'Album': Importer,
+ 'Work': Importer,
}
diff --git a/funkwhale_api/music/lyrics.py b/funkwhale_api/music/lyrics.py
new file mode 100644
index 0000000000000000000000000000000000000000..1ad69ce25f2e2f7e5f4693ee65c28e6e84647ec8
--- /dev/null
+++ b/funkwhale_api/music/lyrics.py
@@ -0,0 +1,31 @@
+import urllib.request
+import html.parser
+from bs4 import BeautifulSoup
+
+
+def _get_html(url):
+ with urllib.request.urlopen(url) as response:
+ html = response.read()
+ return html.decode('utf-8')
+
+
+def extract_content(html):
+ soup = BeautifulSoup(html, "html.parser")
+ return soup.find_all("div", class_='lyricbox')[0].contents
+
+
+def clean_content(contents):
+ final_content = ""
+ for e in contents:
+ if e == '\n':
+ continue
+ if e.name == 'script':
+ continue
+ if e.name == 'br':
+ final_content += "\n"
+ continue
+ try:
+ final_content += e.text
+ except AttributeError:
+ final_content += str(e)
+ return final_content
diff --git a/funkwhale_api/music/migrations/0009_auto_20160920_1614.py b/funkwhale_api/music/migrations/0009_auto_20160920_1614.py
new file mode 100644
index 0000000000000000000000000000000000000000..2046a712765303f4044b627abed3813bf784a539
--- /dev/null
+++ b/funkwhale_api/music/migrations/0009_auto_20160920_1614.py
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.utils.timezone
+import versatileimagefield.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('music', '0008_auto_20160529_1456'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Lyrics',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, verbose_name='ID', serialize=False)),
+ ('url', models.URLField()),
+ ('content', models.TextField(null=True, blank=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Work',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, verbose_name='ID', serialize=False)),
+ ('mbid', models.UUIDField(unique=True, null=True, db_index=True, blank=True)),
+ ('creation_date', models.DateTimeField(default=django.utils.timezone.now)),
+ ('language', models.CharField(max_length=20)),
+ ('nature', models.CharField(max_length=50)),
+ ('title', models.CharField(max_length=255)),
+ ],
+ options={
+ 'ordering': ['-creation_date'],
+ 'abstract': False,
+ },
+ ),
+ migrations.AddField(
+ model_name='lyrics',
+ name='work',
+ field=models.ForeignKey(related_name='lyrics', to='music.Work', blank=True, null=True),
+ ),
+ migrations.AddField(
+ model_name='track',
+ name='work',
+ field=models.ForeignKey(related_name='tracks', to='music.Work', blank=True, null=True),
+ ),
+ ]
diff --git a/funkwhale_api/music/migrations/0010_auto_20160920_1742.py b/funkwhale_api/music/migrations/0010_auto_20160920_1742.py
new file mode 100644
index 0000000000000000000000000000000000000000..03ac057935ff16c90fdcbc479c548b95dadf13eb
--- /dev/null
+++ b/funkwhale_api/music/migrations/0010_auto_20160920_1742.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import versatileimagefield.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('music', '0009_auto_20160920_1614'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='lyrics',
+ name='url',
+ field=models.URLField(unique=True),
+ ),
+ ]
diff --git a/funkwhale_api/music/models.py b/funkwhale_api/music/models.py
index fa2320b05d587b25a76161ac0714f45be0026db7..90d369a7dd6e74afc60fbc705953c3b08a117513 100644
--- a/funkwhale_api/music/models.py
+++ b/funkwhale_api/music/models.py
@@ -4,6 +4,7 @@ import arrow
import datetime
import tempfile
import shutil
+import markdown
from django.conf import settings
from django.db import models
@@ -19,6 +20,8 @@ from funkwhale_api.taskapp import celery
from funkwhale_api import downloader
from funkwhale_api import musicbrainz
from . import importers
+from . import lyrics as lyrics_utils
+
class APIModelMixin(models.Model):
mbid = models.UUIDField(unique=True, db_index=True, null=True, blank=True)
@@ -175,14 +178,99 @@ def import_album(v):
a = Album.get_or_create_from_api(mbid=v[0]['id'])[0]
return a
+
+def link_recordings(instance, cleaned_data, raw_data):
+ tracks = [
+ r['target']
+ for r in raw_data['recording-relation-list']
+ ]
+ Track.objects.filter(mbid__in=tracks).update(work=instance)
+
+
+def import_lyrics(instance, cleaned_data, raw_data):
+ try:
+ url = [
+ url_data
+ for url_data in raw_data['url-relation-list']
+ if url_data['type'] == 'lyrics'
+ ][0]['target']
+ except (IndexError, KeyError):
+ return
+ l, _ = Lyrics.objects.get_or_create(work=instance, url=url)
+
+ return l
+
+
+class Work(APIModelMixin):
+ language = models.CharField(max_length=20)
+ nature = models.CharField(max_length=50)
+ title = models.CharField(max_length=255)
+
+ api = musicbrainz.api.works
+ api_includes = ['url-rels', 'recording-rels']
+ musicbrainz_model = 'work'
+ musicbrainz_mapping = {
+ 'mbid': {
+ 'musicbrainz_field_name': 'id'
+ },
+ 'title': {
+ 'musicbrainz_field_name': 'title'
+ },
+ 'language': {
+ 'musicbrainz_field_name': 'language',
+ },
+ 'nature': {
+ 'musicbrainz_field_name': 'type',
+ 'converter': lambda v: v.lower(),
+ },
+ }
+ import_hooks = [
+ import_lyrics,
+ link_recordings
+ ]
+
+ def fetch_lyrics(self):
+ l = self.lyrics.first()
+ if l:
+ return l
+ data = self.api.get(self.mbid, includes=['url-rels'])['work']
+ l = import_lyrics(self, {}, data)
+
+ return l
+
+
+class Lyrics(models.Model):
+ work = models.ForeignKey(Work, related_name='lyrics', null=True, blank=True)
+ url = models.URLField(unique=True)
+ content = models.TextField(null=True, blank=True)
+
+ @celery.app.task(name='ImportJob.run', filter=celery.task_method)
+ def fetch_content(self):
+ html = lyrics_utils._get_html(self.url)
+ content = lyrics_utils.extract_content(html)
+ cleaned_content = lyrics_utils.clean_content(content)
+ self.content = cleaned_content
+ self.save()
+
+ @property
+ def content_rendered(self):
+ return markdown.markdown(
+ self.content,
+ safe_mode=True,
+ enable_attributes=False,
+ extensions=['markdown.extensions.nl2br'])
+
+
class Track(APIModelMixin):
title = models.CharField(max_length=255)
artist = models.ForeignKey(Artist, related_name='tracks')
position = models.PositiveIntegerField(null=True, blank=True)
album = models.ForeignKey(Album, related_name='tracks', null=True, blank=True)
+ work = models.ForeignKey(Work, related_name='tracks', null=True, blank=True)
+
musicbrainz_model = 'recording'
api = musicbrainz.api.recordings
- api_includes = ['artist-credits', 'releases', 'media', 'tags']
+ api_includes = ['artist-credits', 'releases', 'media', 'tags', 'work-rels']
musicbrainz_mapping = {
'mbid': {
'musicbrainz_field_name': 'id'
@@ -214,6 +302,17 @@ class Track(APIModelMixin):
self.artist = self.album.artist
super().save(**kwargs)
+ def get_work(self):
+ if self.work:
+ return self.work
+ data = self.api.get(self.mbid, includes=['work-rels'])
+ try:
+ work_data = data['recording']['work-relation-list'][0]['work']
+ except (IndexError, KeyError):
+ raise
+ work, _ = Work.get_or_create_from_api(mbid=work_data['id'])
+ return work
+
class TrackFile(models.Model):
track = models.ForeignKey(Track, related_name='files')
audio_file = models.FileField(upload_to='tracks/%Y/%m/%d')
diff --git a/funkwhale_api/music/serializers.py b/funkwhale_api/music/serializers.py
index bb3788157004f40bb068c8705c5ee064b42c5fc5..98faa009963e8316d31f9915b973e1e5f05779a2 100644
--- a/funkwhale_api/music/serializers.py
+++ b/funkwhale_api/music/serializers.py
@@ -81,3 +81,9 @@ class ArtistSerializerNested(serializers.ModelSerializer):
class Meta:
model = models.Artist
fields = ('id', 'mbid', 'name', 'albums', 'tags')
+
+
+class LyricsSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = models.Lyrics
+ fields = ('id', 'work', 'content', 'content_rendered')
diff --git a/funkwhale_api/music/tests/data.py b/funkwhale_api/music/tests/data.py
index 5f3d3546b980c41d800492b254f9996fd2d3f357..54da6bc846190992dd2bd24fde94aaeced0b1a63 100644
--- a/funkwhale_api/music/tests/data.py
+++ b/funkwhale_api/music/tests/data.py
@@ -470,3 +470,33 @@ tracks['search']['8bitadventures'] = {
}
tracks['get']['8bitadventures'] = {'recording': tracks['search']['8bitadventures']['recording-list'][0]}
+tracks['get']['chop_suey'] = {
+ 'recording': {
+ 'id': '46c7368a-013a-47b6-97cc-e55e7ab25213',
+ 'length': '210240',
+ 'title': 'Chop Suey!',
+ 'work-relation-list': [{'target': 'e2ecabc4-1b9d-30b2-8f30-3596ec423dc5',
+ 'type': 'performance',
+ 'type-id': 'a3005666-a872-32c3-ad06-98af558e99b0',
+ 'work': {'id': 'e2ecabc4-1b9d-30b2-8f30-3596ec423dc5',
+ 'language': 'eng',
+ 'title': 'Chop Suey!'}}]}}
+
+works = {'search': {}, 'get': {}}
+works['get']['chop_suey'] = {'work': {'id': 'e2ecabc4-1b9d-30b2-8f30-3596ec423dc5',
+ 'language': 'eng',
+ 'recording-relation-list': [{'direction': 'backward',
+ 'recording': {'disambiguation': 'edit',
+ 'id': '07ca77cf-f513-4e9c-b190-d7e24bbad448',
+ 'length': '170893',
+ 'title': 'Chop Suey!'},
+ 'target': '07ca77cf-f513-4e9c-b190-d7e24bbad448',
+ 'type': 'performance',
+ 'type-id': 'a3005666-a872-32c3-ad06-98af558e99b0'},
+ ],
+ 'title': 'Chop Suey!',
+ 'type': 'Song',
+ 'url-relation-list': [{'direction': 'backward',
+ 'target': 'http://lyrics.wikia.com/System_Of_A_Down:Chop_Suey!',
+ 'type': 'lyrics',
+ 'type-id': 'e38e65aa-75e0-42ba-ace0-072aeb91a538'}]}}
diff --git a/funkwhale_api/music/tests/mocking/lyricswiki.py b/funkwhale_api/music/tests/mocking/lyricswiki.py
new file mode 100644
index 0000000000000000000000000000000000000000..360a7174f0740ff33ab9aa85dedc622e1dd176ef
--- /dev/null
+++ b/funkwhale_api/music/tests/mocking/lyricswiki.py
@@ -0,0 +1,32 @@
+content = """<!doctype html>
+<html lang="en" dir="ltr">
+<head>
+
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+<meta name="generator" content="MediaWiki 1.19.24" />
+<meta name="keywords" content="Chop Suey! lyrics,System Of A Down Chop Suey! lyrics,Chop Suey! by System Of A Down lyrics,lyrics,LyricWiki,LyricWikia,lyricwiki,System Of A Down:Chop Suey!,System Of A Down,System Of A Down:Toxicity (2001),Enter Shikari,Enter Shikari:Chop Suey!,"Weird Al" Yankovic,"Weird Al" Yankovic:Angry White Boy Polka,Renard,Renard:Physicality,System Of A Down:Chop Suey!/pt,Daron Malakian" />
+<meta name="description" content="Chop Suey! This song is by System of a Down and appears on the album Toxicity (2001)." />
+<meta name="twitter:card" content="summary" />
+<meta name="twitter:site" content="@Wikia" />
+<meta name="twitter:url" content="http://lyrics.wikia.com/wiki/System_Of_A_Down:Chop_Suey!" />
+<meta name="twitter:title" content="System Of A Down:Chop Suey! Lyrics - LyricWikia - Wikia" />
+<meta name="twitter:description" content="Chop Suey! This song is by System of a Down and appears on the album Toxicity (2001)." />
+<link rel="canonical" href="http://lyrics.wikia.com/wiki/System_Of_A_Down:Chop_Suey!" />
+<link rel="alternate" type="application/x-wiki" title="Edit" href="/wiki/System_Of_A_Down:Chop_Suey!?action=edit" />
+<link rel="edit" title="Edit" href="/wiki/System_Of_A_Down:Chop_Suey!?action=edit" />
+<link rel="apple-touch-icon" href="http://img4.wikia.nocookie.net/__cb22/lyricwiki/images/b/bc/Wiki.png" />
+<link rel="shortcut icon" href="http://slot1.images.wikia.nocookie.net/__cb1474018633/common/skins/common/images/favicon.ico" />
+<link rel="search" type="application/opensearchdescription+xml" href="/opensearch_desc.php" title="LyricWikia (en)" />
+<link rel="EditURI" type="application/rsd+xml" href="http://lyrics.wikia.com/api.php?action=rsd" />
+<link rel="copyright" href="/wiki/LyricWiki:Copyrights" />
+<link rel="alternate" type="application/atom+xml" title="LyricWikia Atom feed" href="/wiki/Special:RecentChanges?feed=atom" />
+<title>System Of A Down:Chop Suey! Lyrics - LyricWikia - Wikia</title>
+
+<body>
+<div class='lyricbox'>
+<i>We're rolling "Suicide".</i><br /><br />Wake up <i>(wake up)</i><br />Grab a brush and put on a little makeup<br />Hide the scars to fade away the shakeup <i>(hide the scars to fade away the)</i><br />Why'd you leave the keys upon the table?<br />Here you go, create another fable<br /><br />You wanted to<br />Grab a brush and put a little makeup<br />You wanted to<br />Hide the scars to fade away the shakeup<br />You wanted to<br />Why'd you leave the keys upon the table?<br />You wanted to<br /><br />I don't think you trust<br />In my self-righteous suicide<br />I cry when angels deserve to die<br /><br />Wake up <i>(wake up)</i><br />Grab a brush and put on a little makeup<br />Hide the scars to fade away the <i>(hide the scars to fade away the)</i><br />Why'd you leave the keys upon the table?<br />Here you go, create another fable<br /><br />You wanted to<br />Grab a brush and put a little makeup<br />You wanted to<br />Hide the scars to fade away the shakeup<br />You wanted to<br />Why'd you leave the keys upon the table?<br />You wanted to<br /><br />I don't think you trust<br />In my self-righteous suicide<br />I cry when angels deserve to die<br />In my self-righteous suicide<br />I cry when angels deserve to die<br /><br />Father <i>(father)</i><br />Father <i>(father)</i><br />Father <i>(father)</i><br />Father <i>(father)</i><br />Father, into your hands I commit my spirit<br />Father, into your hands<br /><br />Why have you forsaken me?<br />In your eyes forsaken me<br />In your thoughts forsaken me<br />In your heart forsaken me, oh<br /><br />Trust in my self-righteous suicide<br />I cry when angels deserve to die<br />In my self-righteous suicide<br />I cry when angels deserve to die
+</div>
+</body>
+</html>
+"""
diff --git a/funkwhale_api/music/tests/test_lyrics.py b/funkwhale_api/music/tests/test_lyrics.py
new file mode 100644
index 0000000000000000000000000000000000000000..f74ecb2ef6bd37e751667729b8ae56727d73a907
--- /dev/null
+++ b/funkwhale_api/music/tests/test_lyrics.py
@@ -0,0 +1,75 @@
+import json
+import unittest
+from test_plus.test import TestCase
+from django.core.urlresolvers import reverse
+from model_mommy import mommy
+
+from funkwhale_api.music import models
+from funkwhale_api.musicbrainz import api
+from funkwhale_api.music import serializers
+from funkwhale_api.users.models import User
+
+from .mocking import lyricswiki
+from . import data as api_data
+from funkwhale_api.music import lyrics as lyrics_utils
+
+class TestLyrics(TestCase):
+
+ @unittest.mock.patch('funkwhale_api.music.lyrics._get_html',
+ return_value=lyricswiki.content)
+ def test_works_import_lyrics_if_any(self, *mocks):
+ lyrics = mommy.make(
+ models.Lyrics,
+ url='http://lyrics.wikia.com/System_Of_A_Down:Chop_Suey!')
+
+ lyrics.fetch_content()
+ self.assertIn(
+ 'Grab a brush and put on a little makeup',
+ lyrics.content,
+ )
+
+ def test_clean_content(self):
+ c = """<div class="lyricbox">Hello<br /><script>alert('hello');</script>Is it me you're looking for?<br /></div>"""
+ d = lyrics_utils.extract_content(c)
+ d = lyrics_utils.clean_content(d)
+
+ expected = """Hello
+Is it me you're looking for?
+"""
+ self.assertEqual(d, expected)
+
+ def test_markdown_rendering(self):
+ content = """Hello
+Is it me you're looking for?"""
+
+ l = mommy.make(models.Lyrics, content=content)
+
+ expected = "<p>Hello<br />Is it me you're looking for?</p>"
+ self.assertHTMLEqual(expected, l.content_rendered)
+
+ @unittest.mock.patch('funkwhale_api.musicbrainz.api.works.get',
+ return_value=api_data.works['get']['chop_suey'])
+ @unittest.mock.patch('funkwhale_api.musicbrainz.api.recordings.get',
+ return_value=api_data.tracks['get']['chop_suey'])
+ @unittest.mock.patch('funkwhale_api.music.lyrics._get_html',
+ return_value=lyricswiki.content)
+ def test_works_import_lyrics_if_any(self, *mocks):
+ track = mommy.make(
+ models.Track,
+ work=None,
+ mbid='07ca77cf-f513-4e9c-b190-d7e24bbad448')
+
+ url = reverse('api:tracks-lyrics', kwargs={'pk': track.pk})
+ user = User.objects.create_user(
+ username='test', email='test@test.com', password='test')
+ self.client.login(username=user.username, password='test')
+ response = self.client.get(url)
+
+ self.assertEqual(response.status_code, 200)
+
+ track.refresh_from_db()
+ lyrics = models.Lyrics.objects.latest('id')
+ work = models.Work.objects.latest('id')
+
+ self.assertEqual(track.work, work)
+ self.assertEqual(lyrics.work, work)
diff --git a/funkwhale_api/music/tests/test_works.py b/funkwhale_api/music/tests/test_works.py
new file mode 100644
index 0000000000000000000000000000000000000000..84cb51cde09a54c131f76636f6d13545381cd063
--- /dev/null
+++ b/funkwhale_api/music/tests/test_works.py
@@ -0,0 +1,66 @@
+import json
+import unittest
+from test_plus.test import TestCase
+from django.core.urlresolvers import reverse
+from model_mommy import mommy
+
+from funkwhale_api.music import models
+from funkwhale_api.musicbrainz import api
+from funkwhale_api.music import serializers
+from funkwhale_api.users.models import User
+
+from . import data as api_data
+
+class TestWorks(TestCase):
+
+ @unittest.mock.patch('funkwhale_api.musicbrainz.api.works.get',
+ return_value=api_data.works['get']['chop_suey'])
+ def test_can_import_work(self, *mocks):
+ recording = mommy.make(
+ models.Track, mbid='07ca77cf-f513-4e9c-b190-d7e24bbad448')
+ mbid = 'e2ecabc4-1b9d-30b2-8f30-3596ec423dc5'
+ work = models.Work.create_from_api(id=mbid)
+
+ self.assertEqual(work.title, 'Chop Suey!')
+ self.assertEqual(work.nature, 'song')
+ self.assertEqual(work.language, 'eng')
+ self.assertEqual(work.mbid, mbid)
+
+ # a imported work should also be linked to corresponding recordings
+
+ recording.refresh_from_db()
+ self.assertEqual(recording.work, work)
+
+ @unittest.mock.patch('funkwhale_api.musicbrainz.api.works.get',
+ return_value=api_data.works['get']['chop_suey'])
+ @unittest.mock.patch('funkwhale_api.musicbrainz.api.recordings.get',
+ return_value=api_data.tracks['get']['chop_suey'])
+ def test_can_get_work_from_recording(self, *mocks):
+ recording = mommy.make(
+ models.Track,
+ work=None,
+ mbid='07ca77cf-f513-4e9c-b190-d7e24bbad448')
+ mbid = 'e2ecabc4-1b9d-30b2-8f30-3596ec423dc5'
+
+ self.assertEqual(recording.work, None)
+
+ work = recording.get_work()
+
+ self.assertEqual(work.title, 'Chop Suey!')
+ self.assertEqual(work.nature, 'song')
+ self.assertEqual(work.language, 'eng')
+ self.assertEqual(work.mbid, mbid)
+
+ recording.refresh_from_db()
+ self.assertEqual(recording.work, work)
+
+ @unittest.mock.patch('funkwhale_api.musicbrainz.api.works.get',
+ return_value=api_data.works['get']['chop_suey'])
+ def test_works_import_lyrics_if_any(self, *mocks):
+ mbid = 'e2ecabc4-1b9d-30b2-8f30-3596ec423dc5'
+ work = models.Work.create_from_api(id=mbid)
+
+ lyrics = models.Lyrics.objects.latest('id')
+ self.assertEqual(lyrics.work, work)
+ self.assertEqual(
+ lyrics.url, 'http://lyrics.wikia.com/System_Of_A_Down:Chop_Suey!')
diff --git a/funkwhale_api/music/views.py b/funkwhale_api/music/views.py
index 6c5b38db305723458d1e288e3c0c64f2cd0df967..1656754214a70bfe57033554586d4bbfdf422049 100644
--- a/funkwhale_api/music/views.py
+++ b/funkwhale_api/music/views.py
@@ -94,6 +94,27 @@ class TrackViewSet(TagViewSetMixin, SearchMixin, viewsets.ReadOnlyModelViewSet):
return queryset
+ @detail_route(methods=['get'])
+ @transaction.non_atomic_requests
+ def lyrics(self, request, *args, **kwargs):
+ try:
+ track = models.Track.objects.get(pk=kwargs['pk'])
+ except models.Track.DoesNotExist:
+ return Response(status=404)
+
+ work = track.work
+ if not work:
+ work = track.get_work()
+
+ lyrics = work.fetch_lyrics()
+ try:
+ if not lyrics.content:
+ lyrics.fetch_content()
+ except AttributeError:
+ return Response({'error': 'unavailable lyrics'}, status=404)
+ serializer = serializers.LyricsSerializer(lyrics)
+ return Response(serializer.data)
+
class TagViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Tag.objects.all()
diff --git a/funkwhale_api/musicbrainz/client.py b/funkwhale_api/musicbrainz/client.py
index 5fb70b7372e9f77dd28c0a63a37c326b09365d09..34eb9a9d251b9559e2463f247b2efa5e6d98da09 100644
--- a/funkwhale_api/musicbrainz/client.py
+++ b/funkwhale_api/musicbrainz/client.py
@@ -21,11 +21,15 @@ class API(object):
class images(object):
get_front = _api.get_image_front
-
+
class recordings(object):
search = _api.search_recordings
get = _api.get_recording_by_id
+ class works(object):
+ search = _api.search_works
+ get = _api.get_work_by_id
+
class releases(object):
search = _api.search_releases
get = _api.get_release_by_id
diff --git a/requirements/base.txt b/requirements/base.txt
index c8ec2bd71a930912a71837715c47b067c263ff3a..659d0e440788d8742082447d3335cd59661cfae1 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -55,3 +55,5 @@ persisting_theory
django-versatileimagefield
django-cachalot
django-rest-auth
+beautifulsoup4
+markdown