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
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
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
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
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) )
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})
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
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
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))
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")
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
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
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.")
def log_password_reset(sender, user): if not user: return add_user_log(user.id, user.id, "user", "info", "Your password has been changed !")
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")