diff --git a/Pipfile b/Pipfile index e87d112..d67221e 100644 --- a/Pipfile +++ b/Pipfile @@ -4,11 +4,32 @@ verify_ssl = true name = "pypi" [packages] +ffmpeg-python = "~=0.2.0" +loguru = "~=0.7.2" +werkzeug = "~=2.3.7" +python-dotenv = "~=1.0.0" flask = "*" -pydub = "*" -werkzeug = "*" +gunicorn = "*" [dev-packages] +pytest = "~=7.4.0" +pytest-cov = "~=4.1.0" +black = "~=23.7.0" +flake8 = "~=6.1.0" +mypy = "~=1.5.1" +isort = "~=5.12.0" [requires] -python_version = "3.12" +python_version = "3.9" + +[scripts] +start = "gunicorn --config gunicorn.conf.py wsgi:application" +dev = "flask run --debug" +test = "pytest" +lint = "flake8 ." +format = "black ." +check = "mypy ." +sort = "isort ." + +[pipenv] +allow_prereleases = false diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index f0284ea..0000000 --- a/Pipfile.lock +++ /dev/null @@ -1,154 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "d4c91b93113996e6ed5e3506a5d27334a611c8cb5ea2de889bdf4731e6367435" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.12" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "blinker": { - "hashes": [ - "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", - "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc" - ], - "markers": "python_version >= '3.9'", - "version": "==1.9.0" - }, - "click": { - "hashes": [ - "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", - "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" - ], - "markers": "python_version >= '3.7'", - "version": "==8.1.7" - }, - "colorama": { - "hashes": [ - "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", - "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", - "version": "==0.4.6" - }, - "flask": { - "hashes": [ - "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac", - "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136" - ], - "index": "pypi", - "markers": "python_version >= '3.9'", - "version": "==3.1.0" - }, - "itsdangerous": { - "hashes": [ - "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", - "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173" - ], - "markers": "python_version >= '3.8'", - "version": "==2.2.0" - }, - "jinja2": { - "hashes": [ - "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", - "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d" - ], - "markers": "python_version >= '3.7'", - "version": "==3.1.4" - }, - "markupsafe": { - "hashes": [ - "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", - "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", - "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", - "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", - "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", - "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", - "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", - "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", - "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", - "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", - "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", - "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", - "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", - "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", - "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", - "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", - "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", - "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", - "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", - "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", - "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", - "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", - "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", - "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", - "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", - "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", - "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", - "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", - "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", - "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", - "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", - "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", - "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", - "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", - "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", - "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", - "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", - "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", - "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", - "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", - "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", - "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", - "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", - "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", - "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", - "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", - "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", - "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", - "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", - "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", - "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", - "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", - "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", - "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", - "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", - "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", - "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", - "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", - "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", - "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", - "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50" - ], - "markers": "python_version >= '3.9'", - "version": "==3.0.2" - }, - "pydub": { - "hashes": [ - "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6", - "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f" - ], - "index": "pypi", - "version": "==0.25.1" - }, - "werkzeug": { - "hashes": [ - "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", - "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746" - ], - "index": "pypi", - "markers": "python_version >= '3.9'", - "version": "==3.1.3" - } - }, - "develop": {} -} diff --git a/Procfile b/Procfile index 70c173c..878c8d2 100644 --- a/Procfile +++ b/Procfile @@ -1 +1,7 @@ -web: flask run --host=0.0.0.0 --port=8000 \ No newline at end of file +web: gunicorn --config gunicorn.conf.py app:app + +# Uncomment this `release` process if you are using a database, so that Django's model +# migrations are run as part of app deployment, using Heroku's Release Phase feature: +# https://docs.djangoproject.com/en/5.1/topics/migrations/ +# https://devcenter.heroku.com/articles/release-phase +#release: ./manage.py migrate --no-input diff --git a/Readme.md b/Readme.md index 71bea42..0ccb374 100644 --- a/Readme.md +++ b/Readme.md @@ -2,8 +2,6 @@ This project is a web application that allows users to upload audio or video files and reverses the audio track for playback. It supports a wide range of file formats that FFmpeg can handle, includes secure file handling, and can be deployed using Flask's built-in server. -[chatgpt](https://chatgpt.com/share/6751dd60-5754-8013-a93b-8d2cf8f5f8ca) - ## Features - **Multiple File Formats**: Supports any audio or video file format that FFmpeg can process. @@ -18,6 +16,7 @@ This project is a web application that allows users to upload audio or video fil - **Web Server**: Flask's built-in development server - **Storage**: Local file storage for uploads and reversed files - **Session Management**: Flask's built-in session management +- **Frontend**: HTML, CSS, JavaScript with IndexedDB for local storage ## Prerequisites @@ -103,7 +102,7 @@ This project is a web application that allows users to upload audio or video fil - If `requirements.txt` is not available, manually install: ```bash - pip install flask pydub + pip install flask gunicorn ffmpeg-python werkzeug ``` ### 4. Configure Environment Variables (if needed) @@ -173,4 +172,4 @@ Logs are stored in `logs/app.log`. For advanced monitoring, consider integrating ## License -This project is licensed under the MIT License. +This project is licensed under the MIT License. \ No newline at end of file diff --git a/app.py b/app.py index 5815419..8a2c70d 100644 --- a/app.py +++ b/app.py @@ -1,72 +1,101 @@ -import os -from flask import Flask, request, send_file, render_template -from pydub import AudioSegment +from flask import Flask, request, jsonify, send_file, render_template, redirect, url_for, session import io -import tempfile -import logging +import os +from werkzeug.utils import secure_filename +import uuid +import ffmpeg +from loguru import logger app = Flask(__name__) -app.logger.setLevel(logging.DEBUG) # Enable debug logging +app.secret_key = 'your_secret_key' # Replace with a secure, random key +app.config['UPLOAD_FOLDER'] = 'uploads' +app.config['REVERSED_FOLDER'] = 'static/reversed' # Static folder to serve files +app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024 # 500 MB upload limit + +# Ensure the directories exist +os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) +os.makedirs(app.config['REVERSED_FOLDER'], exist_ok=True) + +def generate_unique_filename(filename): + """Generate a unique filename to prevent collisions on the server.""" + unique_id = str(uuid.uuid4()) + name, ext = os.path.splitext(filename) + return f"{unique_id}_{secure_filename(name)}{ext}" @app.route('/') -def index(): +def upload_file(): return render_template('index.html') -@app.route('/reverse', methods=['POST']) -def reverse_audio(): +@app.route('/upload', methods=['POST']) +def process_file(): if 'file' not in request.files: - return "No file provided", 400 - + logger.error("No file part in the request") + return jsonify({'error': 'No file part'}), 400 + file = request.files['file'] if file.filename == '': - return "No file selected", 400 + logger.error("No file selected for uploading") + return jsonify({'error': 'No file selected'}), 400 try: - # Create a temporary directory that won't be deleted immediately - temp_dir = tempfile.mkdtemp() - input_path = os.path.join(temp_dir, 'input' + os.path.splitext(file.filename)[1]) - output_path = os.path.join(temp_dir, 'output.mp3') - - # Save uploaded file + # Generate unique filename and save uploaded file + input_filename = generate_unique_filename(file.filename) + input_path = os.path.join(app.config['UPLOAD_FOLDER'], input_filename) file.save(input_path) - app.logger.debug(f"Saved input file to {input_path}") + # Generate output filename + output_filename = f'reversed_{secure_filename(file.filename)}.mp3' + output_path = os.path.join(app.config['REVERSED_FOLDER'], output_filename) + + # Use ffmpeg to reverse the audio + ffmpeg.input(input_path).output(output_path, af='areverse').run() + + # Clean up the input file + os.remove(input_path) + + # Return the reversed audio file directly + return send_file( + output_path, + mimetype='audio/mpeg', + as_attachment=True, + download_name=output_filename + ) + + except Exception as e: + logger.error(f"Error processing file: {str(e)}") + return jsonify({'error': str(e)}), 500 + +@app.route('/clear') +def clear_reversed_files(): + # Delete user-associated files + user_files = session.get('user_files', []) + for file_path in user_files: try: - # Process the audio - input_audio = AudioSegment.from_file(input_path) - app.logger.debug("Successfully loaded audio file") - - reversed_audio = input_audio.reverse() - app.logger.debug("Successfully reversed audio") - - # Export to mp3 - reversed_audio.export(output_path, format="mp3") - app.logger.debug(f"Exported reversed audio to {output_path}") - - # Read the output file and send it - with open(output_path, 'rb') as f: - output_data = io.BytesIO(f.read()) - output_data.seek(0) - - # Clean up temporary files - os.remove(input_path) - os.remove(output_path) - os.rmdir(temp_dir) - - return send_file( - output_data, - mimetype='audio/mpeg', - as_attachment=True, - download_name='reversed.mp3' - ) + if os.path.exists(file_path): + os.remove(file_path) + except Exception as e: + logger.error(f"Error deleting file {file_path}: {e}") + # Clear session data + session.pop('reversed_files', None) + session.pop('user_files', None) + + return redirect(url_for('upload_file')) + +@app.route('/logout') +def logout(): + # Delete user-associated files + user_files = session.get('user_files', []) + for file_path in user_files: + try: + if os.path.exists(file_path): + os.remove(file_path) except Exception as e: - app.logger.error(f"Error processing audio: {str(e)}") - raise + logger.error(f"Error deleting file {file_path}: {e}") - except Exception as e: - app.logger.error(f"Server error: {str(e)}") - return f"An error occurred: {str(e)}", 500 + # Clear session data + session.clear() + return redirect(url_for('upload_file')) if __name__ == '__main__': - app.run(debug=True, host='0.0.0.0', port=8000) \ No newline at end of file + app.run(debug=False, host='0.0.0.0', port=8000) diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..835c6c9 Binary files /dev/null and b/favicon.ico differ diff --git a/logs/app.log b/logs/app.log index e69de29..58311f7 100644 --- a/logs/app.log +++ b/logs/app.log @@ -0,0 +1,34 @@ +An error occurred +Traceback (most recent call last): + File "C:\Python312\Lib\site-packages\flask\app.py", line 917, in full_dispatch_request + rv = self.dispatch_request() + ^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Python312\Lib\site-packages\flask\app.py", line 902, in dispatch_request + return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "G:\Insidious_Meme\github\reverse-audio-webapp\reverse_audio_webapp\app.py", line 78, in favicon + return send_from_directory(os.path.join(app.root_path, 'static'), 'favicon.ico', mimetype='image/vnd.microsoft.icon') + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Python312\Lib\site-packages\flask\helpers.py", line 565, in send_from_directory + return werkzeug.utils.send_from_directory( # type: ignore[return-value] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Python312\Lib\site-packages\werkzeug\utils.py", line 575, in send_from_directory + raise NotFound() +werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. +An error occurred +Traceback (most recent call last): + File "C:\Python312\Lib\site-packages\flask\app.py", line 917, in full_dispatch_request + rv = self.dispatch_request() + ^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Python312\Lib\site-packages\flask\app.py", line 902, in dispatch_request + return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "G:\Insidious_Meme\github\reverse-audio-webapp\reverse_audio_webapp\app.py", line 78, in favicon + return send_from_directory(os.path.join(app.root_path, 'static'), 'favicon.ico', mimetype='image/vnd.microsoft.icon') + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Python312\Lib\site-packages\flask\helpers.py", line 565, in send_from_directory + return werkzeug.utils.send_from_directory( # type: ignore[return-value] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Python312\Lib\site-packages\werkzeug\utils.py", line 575, in send_from_directory + raise NotFound() +werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again. diff --git a/requirements.txt b/requirements.txt index c60b57c..f527ca8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ -Flask==2.2.2 # If you use Flask as the web framework -gunicorn==20.1.0 # Heroku requires a production-grade WSGI server -pydub==0.25.1 # For audio manipulation, like reversing -ffmpeg-python==0.2.0 # If you use ffmpeg for handling audio files +Flask==2.2.2 # Web framework +gunicorn==20.1.0 # Production-grade WSGI server +ffmpeg-python==0.2.0 # For handling audio files with FFmpeg +Werkzeug==2.2.2 # URL and request utilities +loguru==0.5.3 # Logging library \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 3bb8752..52b6808 100644 --- a/templates/index.html +++ b/templates/index.html @@ -5,7 +5,6 @@ Reverse Audio/Video Files