Пример #1
0
def delete(username, soundslug):
    """
    Delete a track.
    ---
    tags:
        - Tracks
    security:
        - OAuth2:
            - write
    parameters:
        - name: username
          in: path
          type: string
          required: true
          description: User username
        - name: soundslug
          in: path
          type: string
          required: true
          description: Track slug
    responses:
        200:
            description: Returns track name.
    """
    current_user = current_token.user
    if not current_user:
        return jsonify({"error": "Unauthorized"}), 403

    # Get the track
    sound = Sound.query.filter(Sound.user_id == current_user.id,
                               Sound.slug == soundslug).first()
    if not sound:
        return jsonify({"error": "Not found"}), 404

    track_name = sound.title
    freed_space = sound.file_size + sound.transcode_file_size

    # Federate Delete
    from tasks import federate_delete_sound

    federate_delete_sound(sound)

    db.session.delete(sound)

    # recompute user quota
    current_user.quota_count = current_user.quota_count - freed_space

    db.session.commit()

    # log
    add_user_log(sound.id, sound.user.id, "sounds", "info",
                 "Deleted {0} -- {1}".format(sound.id, sound.title))

    return jsonify(track_name), 200
Пример #2
0
def change_password():
    """
    Change user password.
    ---
    tags:
        - Accounts
    security:
        - OAuth2:
            - write
    responses:
        200:
            description: fixme.
    """
    user = current_token.user
    if not user:
        return jsonify({"error": "Unauthorized"}), 403

    password = request.form.get("password", None)
    new_password = request.form.get("new_password", None)
    new_password_confirmation = request.form.get("new_password_confirmation",
                                                 None)

    if not password:
        return jsonify({"error": "password missing"}), 400
    if not new_password:
        return jsonify({"error": "new password missing"}), 400
    if not new_password_confirmation:
        return jsonify({"error": "new password confirmation missing"}), 400

    if new_password != new_password_confirmation:
        return jsonify(
            {"error": "new password and confirmation doesn't match"}), 400

    if password == new_password:
        return jsonify({"error": "passwords are identical"}), 400

    new_hash = hash_password(new_password)
    # Check if old password match
    if verify_password(password, user.password):
        # change password
        user.password = new_hash
        db.session.commit()
        add_user_log(user.id, user.id, "user", "info", "Password changed")
        return jsonify({"status": "success"})
    return jsonify({"error": "old password doesn't match"}), 401
Пример #3
0
def delete(username, albumslug):
    """
    Delete album.
    ---
    tags:
        - Albums
    security:
        - OAuth2:
            - write
    parameters:
        - name: username
          in: path
          type: string
          required: true
          description: User username
        - name: albumslug
          in: path
          type: string
          required: true
          description: Album slug
    responses:
        200:
            description: Returns nothing.
    """
    current_user = current_token.user
    if not current_user:
        return jsonify({"error": "Unauthorized"}), 403

    # Get the track
    album = Album.query.filter(Album.user_id == current_user.id,
                               Album.slug == albumslug).first()
    if not album:
        return jsonify({"error": "Not found"}), 404

    album_name = album.title

    db.session.delete(album)
    db.session.commit()

    # log
    add_user_log(album.id, album.user.id, "albums", "info",
                 "Deleted {0} -- {1}".format(album.id, album.title))

    return jsonify(album_name), 200
Пример #4
0
def reset_password_token(token):
    """
    Change user password with token.
    ---
    tags:
        - Accounts
    responses:
        200:
            description: fixme.
    """
    new_password = request.json.get("new_password", None)
    new_password_confirmation = request.json.get("new_password_confirmation",
                                                 None)

    if not new_password:
        return jsonify({"error": "new password missing"}), 400
    if not new_password_confirmation:
        return jsonify({"error": "new password confirmation missing"}), 400

    if new_password != new_password_confirmation:
        return jsonify(
            {"error": "new password and confirmation doesn't match"}), 400

    new_hash = hash_password(new_password)
    # Check if the token is valid
    tok = PasswordResetToken.query.filter(
        PasswordResetToken.token == token).first()
    if not tok:
        return jsonify({"error": "invalid token"}), 404

    if tok.used:
        return jsonify({"error": "token already used"}), 400

    tok.user.password = new_hash
    tok.used = True
    db.session.commit()
    add_user_log(tok.user.id, tok.user.id, "user", "info",
                 "Password have been changed")

    return jsonify({"status": "success"}), 204
Пример #5
0
def work_transcode(sound_id):
    sound = Sound.query.get(sound_id)
    if not sound:
        print("- Cant find sound ID {id} in database".format(id=sound_id))
        return
    if not sound.transcode_needed:
        print("- Sound ID {id} doesn't need transcoding".format(id=sound_id))
        sound.transcode_state = Sound.TRANSCODE_DONE
        db.session.commit()
        add_user_log(
            sound.id,
            sound.user.id,
            "sounds",
            "info",
            "Transcoding not needed for: {0} -- {1}".format(sound.id, sound.title),
        )
        return
    if not sound.transcode_state == Sound.TRANSCODE_WAITING:
        print("- Sound ID {id} transcoding != TRANSCODE_WAITING".format(id=sound_id))
        return

    print("File: {0}: {1}".format(sound.id, sound.title))
    add_user_log(
        sound.id, sound.user.id, "sounds", "info", "Transcoding started for: {0} -- {1}".format(sound.id, sound.title)
    )

    fname = os.path.join(current_app.config["UPLOADED_SOUNDS_DEST"], sound.user.slug, sound.filename)
    _file, _ext = splitext(fname)

    _start = time.time()

    a = AudioSegment.from_file(fname)
    a.export("{0}.mp3".format(_file), format="mp3", bitrate="196k")

    print("From: {0}".format(fname))
    print("Transcoded: {0}.mp3".format(_file))
    elapsed = time.time() - _start
    print("Transcoding done: ({0}) {1}".format(elapsed, duration_human(elapsed)))

    sound.transcode_state = Sound.TRANSCODE_DONE

    info = sound.sound_infos.first()
    info.done_waveform = False

    _a, _b = splitext(sound.filename)
    sound.filename_transcoded = "{0}.mp3".format(_a)

    sound.transcode_file_size = os.path.getsize(f"{_file}.mp3")

    # recompute user quota
    sound.user.quota_count = sound.user.quota_count + sound.transcode_file_size

    db.session.commit()

    add_user_log(
        sound.id, sound.user.id, "sounds", "info", "Transcoding finished for: {0} -- {1}".format(sound.id, sound.title)
    )
Пример #6
0
def retry_processing(username_or_id, soundslug):
    """
    Reset track processing state.
    ---
    tags:
        - Tracks
    parameters:
        - name: user_id
          in: path
          type: integer
          required: true
          description: User ID
        - name: soundslug
          in: path
          type: string
          required: true
          description: Track slug
    responses:
        200:
            description: Returns track id.
    """
    # Get logged in user from bearer token, or None if not logged in
    if current_token:
        current_user = current_token.user
    else:
        current_user = None

    if not current_user:
        return jsonify({"error": "unauthorized"}), 401

    # Get the associated User from url fetch
    track_user = User.query.filter(User.name == username_or_id, User.local.is_(True)).first()
    if not track_user:
        try:
            track_user = User.query.filter(User.flake_id == username_or_id).first()
        except sqlalchemy.exc.DataError:
            return jsonify({"error": "User not found"}), 404
    if not track_user:
        return jsonify({"error": "User not found"}), 404

    if current_user.id != track_user.id:
        return jsonify({"error": "forbidden"}), 403

    sound = Sound.query.filter(Sound.slug == soundslug, Sound.user_id == track_user.id).first()
    if not sound:
        return jsonify({"error": "not found"}), 404

    if sound.transcode_state != Sound.TRANSCODE_ERROR:
        return jsonify({"error": "cannot reset transcode state if no error"}), 503

    # Delete sound info if any
    if sound.sound_infos.count() > 0:
        db.session.delete(sound.sound_infos.one())

    # Reset transcode state if transcode is needed
    if sound.transcode_needed:
        sound.transcode_state = Sound.TRANSCODE_WAITING

    db.session.commit()

    # re-schedule a job push
    from tasks import upload_workflow

    upload_workflow.delay(sound.id)

    # log
    add_user_log(
        sound.id,
        current_user.id,
        "sounds",
        "info",
        "Re-scheduled a processing {0} -- {1}".format(sound.id, sound.title),
    )

    return jsonify({"trackId": sound.id})
Пример #7
0
def upload():
    """
    Create a new track.
    ---
    tags:
        - Tracks
    security:
        - OAuth2:
            - write
    responses:
        200:
            description: Returns the track id and slug.
    """
    errors = {}

    current_user = current_token.user
    if not current_user:
        return jsonify({"error": "Unauthorized"}), 403

    if "file" not in request.files:
        errors["file"] = "No file present"

    if len(errors) > 0:
        return jsonify({"error": errors}), 400

    # Check for user quota already reached
    if current_user.quota_count >= current_user.quota:
        return jsonify({"error": "quota limit reached"}), 507  # Insufficient storage
        # or 509 Bandwitdh Limit Exceeded...

    # Get file, and file size
    file_uploaded = request.files["file"]
    file_uploaded.seek(0, os.SEEK_END)  # ff to the end
    file_size = file_uploaded.tell()
    file_uploaded.seek(0)  # rewind

    if (current_user.quota_count + file_size) > current_user.quota:
        return jsonify({"error": "quota limit reached"}), 507  # Insufficient storage

    # Do the same with the artwork
    if "artwork" in request.files:
        artwork_uploaded = request.files["artwork"]
        artwork_uploaded.seek(0, os.SEEK_END)
        artwork_size = artwork_uploaded.tell()
        artwork_uploaded.seek(0)
        if artwork_size > Reel2bitsDefaults.artwork_size_limit:  # Max size of 2MB
            return jsonify({"error": "artwork too big, 2MB maximum"}), 413  # Request Entity Too Large
    else:
        artwork_uploaded = None

    form = SoundUploadForm()

    if form.validate_on_submit():
        filename_orig = file_uploaded.filename
        filename_hashed = get_hashed_filename(filename_orig)

        # Save the track file
        sounds.save(file_uploaded, folder=current_user.slug, name=filename_hashed)

        rec = Sound()
        rec.filename = filename_hashed
        rec.filename_orig = filename_orig

        # Save the artwork
        if artwork_uploaded:
            artwork_filename = get_hashed_filename(artwork_uploaded.filename)
            artworksounds.save(artwork_uploaded, folder=current_user.slug, name=artwork_filename)
            rec.artwork_filename = artwork_filename

        rec.licence = form.licence.data
        if form.album.data:
            rec.album_id = form.album.data.id
            if not form.album.data.sounds:
                rec.album_order = 0
            else:
                rec.album_order = form.album.data.sounds.count() + 1

        rec.user_id = current_user.id
        if not form.title.data:
            rec.title, _ = splitext(filename_orig)
        else:
            rec.title = form.title.data
        rec.description = form.description.data
        rec.private = form.private.data
        rec.file_size = file_size
        rec.transcode_file_size = 0  # will be filled, if needed in transcoding workflow
        rec.genre = form.genre.data

        # Handle tags
        tags = form.tags.data.split(",")
        # Clean
        tags = [t.strip() for t in tags if t]
        # For each tag get it or create it
        for tag in tags:
            dbt = SoundTag.query.filter(SoundTag.name == tag).first()
            if not dbt:
                dbt = SoundTag(name=tag)
                db.session.add(dbt)
            rec.tags.append(dbt)

        if "flac" in file_uploaded.mimetype or "ogg" in file_uploaded.mimetype or "wav" in file_uploaded.mimetype:
            rec.transcode_state = Sound.TRANSCODE_WAITING
            rec.transcode_needed = True

        db.session.add(rec)

        # recompute user quota
        current_user.quota_count = current_user.quota_count + rec.file_size

        db.session.commit()

        # push the job in queue
        from tasks import upload_workflow

        upload_workflow.delay(rec.id)

        # log
        add_user_log(rec.id, current_user.id, "sounds", "info", "Uploaded {0} -- {1}".format(rec.id, rec.title))

        return jsonify({"id": rec.flake_id, "slug": rec.slug})

    return jsonify({"error": json.dumps(form.errors)}), 400
Пример #8
0
def reset_password():
    """
    Ask for a reset password link by email.
    ---
    tags:
        - Accounts
    responses:
        200:
            description: fixme.
    """
    email = request.args.get("email", None)
    if not email:
        abort(400)

    user = User.query.filter(User.email == email).first()
    if not user:
        abort(404)

    # generate a reset link
    prt = PasswordResetToken()
    prt.token = generate_random_token()
    prt.expires_at = None
    prt.user_id = user.id

    db.session.add(prt)
    db.session.commit()
    add_user_log(user.id, user.id, "user", "info",
                 "Password reset token generated")

    # Send email
    token_link = f"https://{current_app.config['AP_DOMAIN']}/password-reset/{prt.token}"
    msg = Message(subject="Password reset",
                  recipients=[user.email],
                  sender=current_app.config["MAIL_DEFAULT_SENDER"])

    _config = Config.query.first()
    if not _config:
        print("ERROR: cannot get instance Config from database")
    instance = {"name": None, "url": None}
    if _config:
        instance["name"] = _config.app_name
    instance["url"] = current_app.config["REEL2BITS_URL"]
    msg.body = render_template("email/password_reset.txt",
                               token_link=token_link,
                               user=user,
                               instance=instance)
    msg.html = render_template("email/password_reset.html",
                               token_link=token_link,
                               user=user,
                               instance=instance)
    err = None
    mail = current_app.extensions.get("mail")
    if not mail:
        err = "mail extension is none"
    else:
        try:
            mail.send(msg)
        except ConnectionRefusedError as e:
            # TODO: do something about that maybe
            print(f"Error sending mail: {e}")
            err = e
        except smtplib.SMTPRecipientsRefused as e:
            print(f"Error sending mail: {e}")
            err = e
        except smtplib.SMTPException as e:
            print(f"Error sending mail: {e}")
            err = e
        if err:
            add_log(
                "global", "ERROR",
                f"Error sending email for password reset user {user.id}: {err}"
            )
            add_user_log(user.id, user.id, "user", "error",
                         "An error occured while sending email")

    return jsonify({"status": "ok"}), 204
Пример #9
0
def accounts_update_credentials():
    """
    Update user’s own account.
    ---
    tags:
        - Accounts
    security:
        - OAuth2:
            - write
    responses:
        200:
            description: Returns Account with extra Source and Pleroma attributes.
            schema:
                allOf:
                    - $ref: '#/definitions/Account'
                    - $ref: '#/definitions/Source'
                    - $ref: '#/definitions/AccountPleroma'
    """
    current_user = current_token.user

    if not current_user:
        # WTF ?
        return jsonify({"error": "User not found"}), 404

    # Update fields like bio, language, etc.
    if request.json:
        r_lang = request.json.get("lang", None)
        r_fullname = request.json.get("fullname", None)
        r_bio = request.json.get("bio", None)

        if r_lang:
            current_user.locale = r_lang
        if r_fullname:
            current_user.display_name = r_fullname
        if r_bio:
            current_user.actor[0].summary = r_bio
    elif request.files:
        # Update things like user background, profile picture, etc.
        if "avatar" in request.files:
            avatar_uploaded = request.files["avatar"]
            avatar_uploaded.seek(0, os.SEEK_END)
            avatar_size = avatar_uploaded.tell()
            avatar_uploaded.seek(0)
            if avatar_size > Reel2bitsDefaults.avatar_size_limit:
                return jsonify({"error": "artwork too big, 2MB maximum"
                                }), 413  # Request Entity Too Large

            # Delete old avatar if any
            if current_user.avatar_filename:
                old_avatar = os.path.join(
                    current_app.config["UPLOADED_AVATARS_DEST"],
                    current_user.path_avatar())
                if os.path.isfile(old_avatar):
                    os.unlink(old_avatar)
                else:
                    print(
                        f"Error: cannot delete old avatar: {current_user.id} / {current_user.avatar_filename}"
                    )

            # Save new avatar
            avatar_filename = get_hashed_filename(avatar_uploaded.filename)
            avatars.save(avatar_uploaded,
                         folder=current_user.slug,
                         name=avatar_filename)
            current_user.avatar_filename = avatar_filename

    # commit changes
    db.session.commit()

    # log action
    add_user_log(current_user.id, current_user.id, "user", "info",
                 "Edited user profile")

    # trigger a profile update
    send_update_profile(current_user)

    return jsonify(to_json_account(current_user))
Пример #10
0
def upload_workflow(self, sound_id):
    print("UPLOAD WORKFLOW started")

    sound = Sound.query.get(sound_id)
    if not sound:
        print("- Cant find sound ID {id} in database".format(id=sound_id))
        return

    # First, if the sound isn't local, we need to fetch it
    if sound.activity and not sound.activity.local:
        fetch_remote_track(sound_id)
        fetch_remote_artwork(sound_id)
        if not sound.filename.startswith("remote_"):
            print("UPLOAD WORKFLOW had errors")
            add_log("global", "ERROR",
                    f"Error fetching remote track {sound.id}")
            return

    print("METADATAS started")
    metadatas = work_metadatas(sound_id)
    print("METADATAS finished")

    if not metadatas:
        # cannot process further
        sound.transcode_state = Sound.TRANSCODE_ERROR
        db.session.commit()
        print("UPLOAD WORKFLOW had errors")
        add_log("global", "ERROR", f"Error processing track {sound.id}")
        add_user_log(sound.id, sound.user_id, "sounds", "error",
                     "An error occured while processing your track")
        return

    if metadatas:
        print("TRANSCODE started")
        work_transcode(sound_id)
        print("TRANSCODE finished")

    # The rest only applies if the track is local
    if not sound.remote_uri:
        # Federate if public
        if not sound.private:
            print("UPLOAD WORKFLOW federating sound")
            # Federate only if sound is public
            sound.activity_id = federate_new_sound(sound)
            db.session.commit()

        track_url = f"https://{current_app.config['AP_DOMAIN']}/{sound.user.name}/track/{sound.slug}"

        msg = Message(
            subject="Song processing finished",
            recipients=[sound.user.email],
            sender=current_app.config["MAIL_DEFAULT_SENDER"],
        )

        _config = Config.query.first()
        if not _config:
            print("ERROR: cannot get instance Config from database")
        instance = {"name": None, "url": None}
        if _config:
            instance["name"] = _config.app_name
        instance["url"] = current_app.config["REEL2BITS_URL"]
        msg.body = render_template("email/song_processed.txt",
                                   sound=sound,
                                   track_url=track_url,
                                   instance=instance)
        msg.html = render_template("email/song_processed.html",
                                   sound=sound,
                                   track_url=track_url,
                                   instance=instance)
        err = None
        mail = current_app.extensions.get("mail")
        if not mail:
            err = "mail extension is none"
        else:
            try:
                mail.send(msg)
            except ConnectionRefusedError as e:
                # TODO: do something about that maybe
                print(f"Error sending mail: {e}")
                err = e
            except smtplib.SMTPRecipientsRefused as e:
                print(f"Error sending mail: {e}")
                err = e
            except smtplib.SMTPException as e:
                print(f"Error sending mail: {e}")
                err = e
        if err:
            add_log("global", "ERROR",
                    f"Error sending email for track {sound.id}: {err}")
            add_user_log(sound.id, sound.user.id, "sounds", "error",
                         "An error occured while sending email")

    print("UPLOAD WORKFLOW finished")
Пример #11
0
def work_metadatas(sound_id, force=False):
    # force is unused for now
    sound = Sound.query.get(sound_id)
    if not sound:
        print("- Cant find sound ID %(id)s in database".format(id=sound_id))
        return

    add_user_log(
        sound.id,
        sound.user.id,
        "sounds",
        "info",
        "Metadatas gathering started for: {0} -- {1}".format(sound.id, sound.title),
    )

    _infos = sound.sound_infos.first()

    if not _infos:
        _infos = SoundInfo()
        _infos.sound_id = sound.id

    # Generate Basic infos

    fname = os.path.join(current_app.config["UPLOADED_SOUNDS_DEST"], sound.user.slug, sound.filename)
    basic_infos = None
    if not _infos.done_basic:
        basic_infos = get_basic_infos(fname)
        if type(basic_infos) != dict:
            # cannot process further
            print(f"- MIME: '{basic_infos}' is not supported")
            add_log("global", "ERROR", f"Unsupported audio format: {basic_infos}")
            return False

    if not _infos.done_basic or force:
        print("- WORKING BASIC on {0}, {1}".format(sound.id, sound.filename))
        print("- Our file got basic infos: {0}".format(basic_infos))
        _infos.duration = basic_infos["duration"]
        _infos.channels = basic_infos["channels"]
        _infos.rate = basic_infos["rate"]
        _infos.codec = basic_infos["codec"]
        _infos.format = basic_infos["format"]
        _infos.bitrate = basic_infos["bitrate"]
        _infos.bitrate_mode = basic_infos["bitrate_mode"]
        _infos.done_basic = True
        _infos.type = basic_infos["type"]
        _infos.type_human = basic_infos["type_human"]

    if not _infos.done_waveform or force:
        if sound.transcode_state == Sound.TRANSCODE_DONE:
            _f, _e = splitext(fname)
            fname_t = "{0}.mp3".format(_f)
            print("- WORKING ON TRANSCODED FOR WAVEFORM")
        else:
            fname_t = fname

        print("- GENERATING AUDIO DAT FILE")
        dat_file_name = generate_audio_dat_file(fname_t, _infos.duration)

        print("- WORKING WAVEFORM on {0}, {1}".format(sound.id, sound.filename))
        waveform_infos = get_waveform(dat_file_name, _infos.duration)
        print("- Our file got waveform infos: {0}".format(waveform_infos))
        _infos.waveform = waveform_infos
        if not waveform_infos:
            _infos.waveform_error = True
            add_user_log(
                sound.id,
                sound.user.id,
                "sounds",
                "info",
                "Got an error when generating waveform" " for: {0} -- {1}".format(sound.id, sound.title),
            )

        # Delete the temporary dat file
        os.unlink(dat_file_name)

        _infos.done_waveform = True

    db.session.add(_infos)
    db.session.commit()

    add_user_log(
        sound.id,
        sound.user.id,
        "sounds",
        "info",
        "Metadatas gathering finished for: {0} -- {1}".format(sound.id, sound.title),
    )
    return True
Пример #12
0
def new():
    """
    Create a new album.
    ---
    tags:
        - Albums
    security:
        - OAuth2:
            - write
    responses:
        200:
            description: Returns id and slug.
    """
    current_user = current_token.user
    if not current_user:
        return jsonify({"error": "Unauthorized"}), 403

    # Check artwork file size
    if "artwork" in request.files:
        artwork_uploaded = request.files["artwork"]
        artwork_uploaded.seek(0, os.SEEK_END)
        artwork_size = artwork_uploaded.tell()
        artwork_uploaded.seek(0)
        if artwork_size > Reel2bitsDefaults.artwork_size_limit:
            return jsonify({"error": "artwork too big, 2MB maximum"
                            }), 413  # Request Entity Too Large
    else:
        artwork_uploaded = None

    form = AlbumForm()

    if form.validate_on_submit():
        rec = Album()
        rec.user_id = current_user.id
        rec.title = form.title.data
        rec.private = form.private.data
        rec.description = form.description.data
        rec.genre = form.genre.data

        # Save the artwork
        if artwork_uploaded:
            artwork_filename = get_hashed_filename(artwork_uploaded.filename)
            artworkalbums.save(artwork_uploaded,
                               folder=current_user.slug,
                               name=artwork_filename)
            rec.artwork_filename = artwork_filename

        # Handle tags
        tags = form.tags.data.split(",")
        # Clean
        tags = [t.strip() for t in tags if t]
        # For each tag get it or create it
        for tag in tags:
            dbt = SoundTag.query.filter(SoundTag.name == tag).first()
            if not dbt:
                dbt = SoundTag(name=tag)
                db.session.add(dbt)
            rec.tags.append(dbt)

        db.session.add(rec)
        db.session.commit()

        # log
        add_user_log(rec.id, rec.user_id, "albums", "info",
                     "Created {0} -- {1}".format(rec.id, rec.title))

        return jsonify({"id": rec.flake_id, "slug": rec.slug})

    return jsonify({"error": json.dumps(form.errors)}), 400
Пример #13
0
 def log_reset_password_instr(sender, user, token):
     if not user:
         return
     add_user_log(user.id, user.id, "user", "info",
                  "Password reset instructions sent.")
Пример #14
0
 def log_password_reset(sender, user):
     if not user:
         return
     add_user_log(user.id, user.id, "user", "info",
                  "Your password has been changed !")
Пример #15
0
def upload_workflow(self, sound_id):
    print("UPLOAD WORKFLOW started")

    sound = Sound.query.get(sound_id)
    if not sound:
        print("- Cant find sound ID {id} in database".format(id=sound_id))
        return

    print("METADATAS started")
    metadatas = work_metadatas(sound_id)
    print("METADATAS finished")

    if not metadatas:
        # cannot process further
        sound.transcode_state = Sound.TRANSCODE_ERROR
        db.session.commit()
        print("UPLOAD WORKFLOW had errors")
        add_log("global", "ERROR", f"Error processing track {sound.id}")
        add_user_log(sound.id, sound.user.id, "sounds", "error",
                     "An error occured while processing your track")
        return

    if metadatas:
        print("TRANSCODE started")
        work_transcode(sound_id)
        print("TRANSCODE finished")

    # Federate if public
    if not sound.private:
        print("UPLOAD WORKFLOW federating sound")
        # Federate only if sound is public
        sound.activity_id = federate_new_sound(sound)
        db.session.commit()

    track_url = f"https://{current_app.config['AP_DOMAIN']}/{sound.user.name}/track/{sound.slug}"

    msg = Message(
        subject="Song processing finished",
        recipients=[sound.user.email],
        sender=current_app.config["MAIL_DEFAULT_SENDER"],
    )
    msg.body = render_template("email/song_processed.txt",
                               sound=sound,
                               track_url=track_url)
    msg.html = render_template("email/song_processed.html",
                               sound=sound,
                               track_url=track_url)
    err = None
    mail = current_app.extensions.get("mail")
    if not mail:
        err = "mail extension is none"
    else:
        try:
            mail.send(msg)
        except ConnectionRefusedError as e:
            # TODO: do something about that maybe
            print(f"Error sending mail: {e}")
            err = e
        except smtplib.SMTPRecipientsRefused as e:
            print(f"Error sending mail: {e}")
            err = e
        except smtplib.SMTPException as e:
            print(f"Error sending mail: {e}")
            err = e
    if err:
        add_log("global", "ERROR",
                f"Error sending email for track {sound.id}: {err}")
        add_user_log(sound.id, sound.user.id, "sounds", "error",
                     "An error occured while sending email")

    print("UPLOAD WORKFLOW finished")