Skip to content
Snippets Groups Projects
tasks.py 7.67 KiB
Newer Older
  • Learn to ignore specific revisions
  • 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])