From b737365003755874d83030366e77cdcc1568f150 Mon Sep 17 00:00:00 2001 From: Eliot Berriot <contact@eliotberriot.com> Date: Tue, 20 Sep 2016 19:46:18 +0200 Subject: [PATCH] Fixed #8: can now fetch and store lyrics --- config/settings/common.py | 2 +- docker/Dockerfile.local | 4 - funkwhale_api/music/admin.py | 18 +++- funkwhale_api/music/importers.py | 1 + funkwhale_api/music/lyrics.py | 31 ++++++ .../migrations/0009_auto_20160920_1614.py | 49 +++++++++ .../migrations/0010_auto_20160920_1742.py | 20 ++++ funkwhale_api/music/models.py | 101 +++++++++++++++++- funkwhale_api/music/serializers.py | 6 ++ funkwhale_api/music/tests/data.py | 30 ++++++ .../music/tests/mocking/lyricswiki.py | 32 ++++++ funkwhale_api/music/tests/test_lyrics.py | 75 +++++++++++++ funkwhale_api/music/tests/test_works.py | 66 ++++++++++++ funkwhale_api/music/views.py | 21 ++++ funkwhale_api/musicbrainz/client.py | 6 +- requirements/base.txt | 2 + 16 files changed, 456 insertions(+), 8 deletions(-) create mode 100644 funkwhale_api/music/lyrics.py create mode 100644 funkwhale_api/music/migrations/0009_auto_20160920_1614.py create mode 100644 funkwhale_api/music/migrations/0010_auto_20160920_1742.py create mode 100644 funkwhale_api/music/tests/mocking/lyricswiki.py create mode 100644 funkwhale_api/music/tests/test_lyrics.py create mode 100644 funkwhale_api/music/tests/test_works.py diff --git a/config/settings/common.py b/config/settings/common.py index 863954e..9ac3b47 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 12760ea..b704104 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 b18d540..a6a7f94 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 3313193..7e26fe9 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 0000000..1ad69ce --- /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 0000000..2046a71 --- /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 0000000..03ac057 --- /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 fa2320b..90d369a 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 bb37881..98faa00 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 5f3d354..54da6bc 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 0000000..360a717 --- /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 0000000..f74ecb2 --- /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 0000000..84cb51c --- /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 6c5b38d..1656754 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 5fb70b7..34eb9a9 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 c8ec2bd..659d0e4 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -55,3 +55,5 @@ persisting_theory django-versatileimagefield django-cachalot django-rest-auth +beautifulsoup4 +markdown -- GitLab