Skip to content
Snippets Groups Projects
tasks.py 7.67 KiB
Newer Older
from django.core.files.base import ContentFile

from dynamic_preferences.registries import global_preferences_registry

from funkwhale_api.federation import activity
from funkwhale_api.federation import actors
from funkwhale_api.federation import models as federation_models
from funkwhale_api.federation import serializers as federation_serializers
from funkwhale_api.taskapp import celery
from funkwhale_api.providers.acoustid import get_acoustid_client
Eliot Berriot's avatar
Eliot Berriot committed
from funkwhale_api.providers.audiofile.tasks import import_track_data_from_path

from django.conf import settings
from . import models
from . import lyrics as lyrics_utils


@celery.app.task(name='acoustid.set_on_track_file')
@celery.require_instance(models.TrackFile, 'track_file')
def set_acoustid_on_track_file(track_file):
    client = get_acoustid_client()
    result = client.get_best_match(track_file.audio_file.path)

    def update(id):
        track_file.acoustid_track_id = id
        track_file.save(update_fields=['acoustid_track_id'])
        return id
    if result:
        return update(result['id'])


def import_track_from_remote(library_track):
    metadata = library_track.metadata
    try:
        track_mbid = metadata['recording']['musicbrainz_id']
        assert track_mbid  # for null/empty values
    except (KeyError, AssertionError):
        pass
    else:
        return models.Track.get_or_create_from_api(mbid=track_mbid)

    try:
        album_mbid = metadata['release']['musicbrainz_id']
        assert album_mbid  # for null/empty values
    except (KeyError, AssertionError):
        pass
    else:
        album = models.Album.get_or_create_from_api(mbid=album_mbid)
        return models.Track.get_or_create_from_title(
            library_track.title, artist=album.artist, album=album)
    try:
        artist_mbid = metadata['artist']['musicbrainz_id']
        assert artist_mbid  # for null/empty values
    except (KeyError, AssertionError):
        pass
    else:
        artist = models.Artist.get_or_create_from_api(mbid=artist_mbid)
        album = models.Album.get_or_create_from_title(
            library_track.album_title, artist=artist)
        return models.Track.get_or_create_from_title(
            library_track.title, artist=artist, album=album)

    # worst case scenario, we have absolutely no way to link to a
    # musicbrainz resource, we rely on the name/titles
    artist = models.Artist.get_or_create_from_name(
    album = models.Album.get_or_create_from_title(
        library_track.album_title, artist=artist)
    return models.Track.get_or_create_from_title(
        library_track.title, artist=artist, album=album)
def _do_import(import_job, replace=False, use_acoustid=True):
    from_file = bool(import_job.audio_file)
    mbid = import_job.mbid
    acoustid_track_id = None
    duration = None
    track = None
    manager = global_preferences_registry.manager()
    use_acoustid = use_acoustid and manager['providers_acoustid__api_key']
    if not mbid and use_acoustid and from_file:
        # we try to deduce mbid from acoustid
        client = get_acoustid_client()
        match = client.get_best_match(import_job.audio_file.path)
        if match:
            duration = match['recordings'][0]['duration']
            mbid = match['recordings'][0]['id']
            acoustid_track_id = match['id']
    if mbid:
        track, _ = models.Track.get_or_create_from_api(mbid=mbid)
    elif import_job.audio_file:
        track = import_track_data_from_path(import_job.audio_file.path)
    elif import_job.library_track:
        track = import_track_from_remote(import_job.library_track)
    elif import_job.source.startswith('file://'):
        track = import_track_data_from_path(
            import_job.source.replace('file://', '', 1))
        raise ValueError(
            'Not enough data to process import, '
            'add a mbid, an audio file or a library track')

    track_file = None
    if replace:
        track_file = track.files.first()
    elif track.files.count() > 0:
        if import_job.audio_file:
            import_job.audio_file.delete()
        import_job.status = 'skipped'
        import_job.save()
        return

    track_file = track_file or models.TrackFile(
        track=track, source=import_job.source)
    track_file.acoustid_track_id = acoustid_track_id
    if from_file:
        track_file.audio_file = ContentFile(import_job.audio_file.read())
        track_file.audio_file.name = import_job.audio_file.name
        track_file.duration = duration
        track_file.library_track = import_job.library_track
        track_file.mimetype = import_job.library_track.audio_mimetype
        if import_job.library_track.library.download_files:
            raise NotImplementedError()
        else:
            # no downloading, we hotlink
            pass
    elif not import_job.audio_file and not import_job.source.startswith('file://'):
        # not an implace import, and we have a source, so let's download it
        track_file.download_file()
    track_file.save()
    import_job.status = 'finished'
    import_job.track_file = track_file
    if import_job.audio_file:
        # it's imported on the track, we don't need it anymore
        import_job.audio_file.delete()
    import_job.save()
@celery.app.task(name='ImportJob.run', bind=True)
@celery.require_instance(
    models.ImportJob.objects.filter(
        status__in=['pending', 'errored']),
    'import_job')
def import_job_run(self, import_job, replace=False, use_acoustid=True):
    def mark_errored():
        import_job.status = 'errored'
        import_job.save(update_fields=['status'])
        tf = _do_import(import_job, replace, use_acoustid=use_acoustid)
        return tf.pk if tf else None
    except Exception as exc:
        if not settings.DEBUG:
            try:
                self.retry(exc=exc, countdown=30, max_retries=3)
            except:
                mark_errored()
                raise
        mark_errored()
        raise


@celery.app.task(name='Lyrics.fetch_content')
@celery.require_instance(models.Lyrics, 'lyrics')
def fetch_content(lyrics):
    html = lyrics_utils._get_html(lyrics.url)
    content = lyrics_utils.extract_content(html)
    cleaned_content = lyrics_utils.clean_content(content)
    lyrics.content = cleaned_content
    lyrics.save(update_fields=['content'])


@celery.app.task(name='music.import_batch_notify_followers')
@celery.require_instance(
    models.ImportBatch.objects.filter(status='finished'), 'import_batch')
def import_batch_notify_followers(import_batch):
    if not settings.FEDERATION_ENABLED:
        return

    if import_batch.source == 'federation':
        return

    library_actor = actors.SYSTEM_ACTORS['library'].get_actor_instance()
    followers = library_actor.get_approved_followers()
    jobs = import_batch.jobs.filter(
        status='finished',
        library_track__isnull=True,
        track_file__isnull=False,
    ).select_related(
        'track_file__track__artist',
        'track_file__track__album__artist',
    )
    track_files = [job.track_file for job in jobs]
    collection = federation_serializers.CollectionSerializer({
        'actor': library_actor,
        'id': import_batch.get_federation_url(),
        'items': track_files,
        'item_serializer': federation_serializers.AudioSerializer
    }).data
    for f in followers:
        create = federation_serializers.ActivitySerializer(
            {
                'type': 'Create',
                'id': collection['id'],
                'object': collection,
                'actor': library_actor.url,
                'to': [f.url],
            }
        ).data

        activity.deliver(create, on_behalf_of=library_actor, to=[f.url])