diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..f3c73bdfb Binary files /dev/null and b/.DS_Store differ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..6c0a3892c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "appService.defaultWebAppToDeploy": "/subscriptions/2fa1771c-889e-44af-96c2-48ad38845554/resourceGroups/FredImpYouTubeDownloader/providers/Microsoft.Web/sites/FredImpYouTubeDownloader", + "appService.deploySubpath": "." +} \ No newline at end of file diff --git a/app.py b/app.py index 3d1808cf6..224e30b47 100644 --- a/app.py +++ b/app.py @@ -1,32 +1,188 @@ import os +import certifi +import ssl +import zipfile +from functools import wraps +from pytube import YouTube +from pytube.cli import on_progress +from pytube.exceptions import PytubeError, VideoUnavailable, VideoPrivate, LiveStreamError +from flask_oauthlib.client import OAuth +from io import BytesIO +from flask import Flask, redirect, render_template, request, send_file, url_for, session +from dotenv import load_dotenv -from flask import (Flask, redirect, render_template, request, - send_from_directory, url_for) +ssl._create_default_https_context = ssl._create_unverified_context app = Flask(__name__) +secret_key = os.urandom(24) +app.secret_key = secret_key # Change this to a more secure secret key + +oauth = OAuth(app) + +google = oauth.remote_app( + 'google', + consumer_key='4replace, + consumer_secret='replace', + request_token_params={'scope': 'email'}, + base_url='https://www.googleapis.com/oauth2/v1/', + request_token_url=None, + access_token_method='POST', + access_token_url='https://accounts.google.com/o/oauth2/token', + authorize_url='https://accounts.google.com/o/oauth2/auth', +) + +app.config['OAUTH_CREDENTIALS'] = { + 'google': { + 'id': 'replcae', + 'secret': 'repalce', + 'redirect_uri': 'replace', + }, +} + +# Dictionary to store download progress for each user +download_progress = {} + +def login_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if 'user_email' not in session: + return redirect(url_for('login_page', next=request.url)) + return f(*args, **kwargs) + return decorated_function + +@app.route('/login_page') +def login_page(): + if 'user_email' in session: + return redirect(url_for('index')) + return render_template("login.html") @app.route('/') +@login_required def index(): - print('Request for index page received') - return render_template('index.html') + return render_template("index.html") + +@app.route('/login') +def login(): + return google.authorize(callback=url_for('authorized', _external=True)) + +@app.route('/logout') +def logout(): + session.pop('google_token', None) + session.pop('user_email', None) + return redirect(url_for('login_page')) + +@app.route('/login/authorized') +def authorized(): + response = google.authorized_response() + + if response is None or response.get('access_token') is None: + return 'Access denied: reason={} error={}'.format( + request.args['error_reason'], + request.args['error_description'] + ) + + session['google_token'] = (response['access_token'], '') + user_info = google.get('userinfo') + session['user_email'] = user_info.data['email'] + + return redirect(url_for('index')) + +def download_audio_and_video(yt): + try: + audio = yt.streams.filter(only_audio=True, file_extension='mp4').order_by('abr').desc().first() + video = yt.streams.filter(progressive=True, file_extension='mp4').order_by('resolution').desc().first() + + if not audio: + return None, "Couldn't find a suitable audio stream for the provided URL." + if not video: + return None, "Couldn't find a suitable video stream for the provided URL." + + audio_buffer = BytesIO() + video_buffer = BytesIO() -@app.route('/favicon.ico') -def favicon(): - return send_from_directory(os.path.join(app.root_path, 'static'), - 'favicon.ico', mimetype='image/vnd.microsoft.icon') + audio.stream_to_buffer(audio_buffer) + video.stream_to_buffer(video_buffer) + + audio_buffer.seek(0) + video_buffer.seek(0) + + return audio_buffer, video_buffer, None + except PytubeError as e: + return None, None, f"An error occurred while processing the audio and video: {str(e)}" + + try: + audio = yt.streams.filter(only_audio=True).order_by('abr').desc().first() + video = yt.streams.filter(progressive=True, file_extension='mp4').order_by('resolution').desc().first() + + if not audio: + return None, "Couldn't find a suitable audio stream for the provided URL." + if not video: + return None, "Couldn't find a suitable video stream for the provided URL." + + audio_buffer = BytesIO() + video_buffer = BytesIO() + + audio.stream_to_buffer(audio_buffer) + video.stream_to_buffer(video_buffer) + + audio_buffer.seek(0) + video_buffer.seek(0) + + return audio_buffer, video_buffer, None + except PytubeError as e: + return None, None, f"An error occurred while processing the audio and video: {str(e)}" @app.route('/hello', methods=['POST']) +@login_required def hello(): - name = request.form.get('name') + url = request.form.get('name') + if not url: + return render_template("error.html", message="URL is missing. Please provide a valid YouTube URL.", back_url=url_for('index')) + + try: + yt = YouTube(url, on_progress_callback=on_progress) + + audio_buffer, video_buffer, download_error = download_audio_and_video(yt) + if download_error: + return render_template("error.html", message=download_error, back_url=url_for('index')) + + # Create a ZIP archive containing audio and video + zip_buffer = BytesIO() + with zipfile.ZipFile(zip_buffer, 'w') as zip_file: + zip_file.writestr(f"{yt.title}.mp3", audio_buffer.read()) + zip_file.writestr(f"{yt.title}.mp4", video_buffer.read()) + + zip_buffer.seek(0) + + return send_file( + zip_buffer, + as_attachment=True, + attachment_filename=f"{yt.title}.zip", + mimetype="application/zip", + ) + except VideoUnavailable: + return render_template("error.html", message="The provided YouTube video is unavailable.", back_url=url_for('index')) + except VideoPrivate: + return render_template("error.html", message="The provided YouTube video is private and cannot be accessed.", back_url=url_for('index')) + except LiveStreamError: + return render_template("error.html", message="Live streams cannot be downloaded.", back_url=url_for('index')) + except PytubeError as e: + return render_template("error.html", message=f"An error occurred while processing the video: {str(e)}", back_url=url_for('index')) + +@app.route('/error', methods=['GET']) +@login_required +def error(): + message = request.args.get('message') + back_url = request.args.get('back_url') + return render_template("error.html", message=message, back_url=back_url) - if name: - print('Request for hello page received with name=%s' % name) - return render_template('hello.html', name = name) - else: - print('Request for hello page received with no name or blank name -- redirecting') - return redirect(url_for('index')) +@google.tokengetter +def get_google_oauth_token(): + return session.get('google_token') +if __name__ == "__main__": + # Set the SSL certificate file for urllib + os.environ["SSL_CERT_FILE"] = certifi.where() -if __name__ == '__main__': - app.run() + app.run(debug=True, port=5000) diff --git a/requirements.txt b/requirements.txt index a3c7c6ad6..6bd8d6968 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,6 @@ Flask==2.0.2 gunicorn +pytube +certifi +moviepy +Flask-OAuthlib \ No newline at end of file diff --git a/templates/error.html b/templates/error.html new file mode 100644 index 000000000..58a7e10bf --- /dev/null +++ b/templates/error.html @@ -0,0 +1,59 @@ + + + + + Error + + + + +
+

Error

+

{{ message }}

+ Back to Enter URL +
+ + + \ No newline at end of file diff --git a/templates/hello.html b/templates/hello.html index c277e7740..d5fcb0d07 100644 --- a/templates/hello.html +++ b/templates/hello.html @@ -1,21 +1,25 @@ + Hello Azure - Python Quickstart - -
+ + +
- Azure Logo - -

Hello {{name}}

+ Azure Logo +

The YouTube Video with Title {{name}} Has been downloaded

- It is nice to meet you! + {% if 'user_email' in session %} Logged in as: {{ session['user_email'] }} + Logout from Google {% else %} + Login with Google {% endif %}

Back home -
-
- + +
+ + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 9cdba30cd..e7459a060 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,30 +1,49 @@ + + - Hello Azure - Python Quickstart + + + Fred's YouTube Video Audio Downloader + - - -
-
- Azure Logo - -

Welcome to Azure

-
-
-
- - -
- -
-
- -
+ +
+
+
+ Azure Logo +

Fred's YouTube Video and Audio downloader

+
+ +
+
+
+ + +
+
+ +
+
+
+ +
+

Logged in as: {{ session['user_email'] }}

+ Logout from Google
- -
- +
+
+ + \ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 000000000..6a01f0920 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,27 @@ + + + + + + + Fred's YouTube Downloader - Login + + + + + +
+
+
+ Azure Logo +

Welcome to Fred's YouTube Downloader

+

+ Please login to continue. This is just to ensure you are not a bot +

+ Login with Google +
+
+
+ + + \ No newline at end of file diff --git a/templates/progress.html b/templates/progress.html new file mode 100644 index 000000000..e6a8eabd1 --- /dev/null +++ b/templates/progress.html @@ -0,0 +1,73 @@ + + + + + + + Download Progress + + + + + +
+

Download Progress

+
+
+
+
+

Download completed successfully!

+
+
+ + + + + \ No newline at end of file