15分鐘

Flask一個Youtube下載網站

2019/05/26 SIRLA定期聚

by 土豆

成品

git clone https://github.com/SIRLA-FJULIS/FlaTuber

Pytube介紹

pip install pytube
import pytube

url = "https://www.youtube.com/watch?v=elsh3J5lJ6g"
yt = Youtube(url)

video = yt.streams.filter(subtype='mp4').first()

video.download()

查看所有可下載項目

<Stream: itag="18" mime_type="video/mp4" res="360p" fps="30fps" vcodec="avc1.42001E" acodec="mp4a.40.2">
<Stream: itag="137" mime_type="video/mp4" res="1080p" fps="30fps" vcodec="avc1.640028">
<Stream: itag="248" mime_type="video/webm" res="1080p" fps="30fps" vcodec="vp9">
<Stream: itag="136" mime_type="video/mp4" res="720p" fps="30fps" vcodec="avc1.4d401f">
<Stream: itag="247" mime_type="video/webm" res="720p" fps="30fps" vcodec="vp9">
<Stream: itag="135" mime_type="video/mp4" res="480p" fps="30fps" vcodec="avc1.4d401e">
<Stream: itag="244" mime_type="video/webm" res="480p" fps="30fps" vcodec="vp9">
<Stream: itag="134" mime_type="video/mp4" res="360p" fps="30fps" vcodec="avc1.4d401e">
<Stream: itag="243" mime_type="video/webm" res="360p" fps="30fps" vcodec="vp9">
<Stream: itag="133" mime_type="video/mp4" res="240p" fps="30fps" vcodec="avc1.4d4015">
<Stream: itag="242" mime_type="video/webm" res="240p" fps="30fps" vcodec="vp9">
<Stream: itag="160" mime_type="video/mp4" res="144p" fps="30fps" vcodec="avc1.4d400c">
<Stream: itag="278" mime_type="video/webm" res="144p" fps="30fps" vcodec="vp9">
<Stream: itag="140" mime_type="audio/mp4" abr="128kbps" acodec="mp4a.40.2">
<Stream: itag="171" mime_type="audio/webm" abr="128kbps" acodec="vorbis">
<Stream: itag="249" mime_type="audio/webm" abr="50kbps" acodec="opus">
<Stream: itag="250" mime_type="audio/webm" abr="70kbps" acodec="opus">
<Stream: itag="251" mime_type="audio/webm" abr="160kbps" acodec="opus">
yt.streams.all()

Progressive vs. DASH

<Stream: itag="18" mime_type="video/mp4" res="360p" fps="30fps" vcodec="avc1.42001E" acodec="mp4a.40.2">
<Stream: itag="137" mime_type="video/mp4" res="1080p" fps="30fps" vcodec="avc1.640028">
<Stream: itag="251" mime_type="audio/webm" abr="160kbps" acodec="opus">

DASH: Dynamic Adaptive Streaming over HTTP

讓高品質影像播放更加流暢,但聲音與影像會分開

Progressive

分段式,載入較慢,但影音合在一起,最高只支援到720p

filter()

yt.streams.filter(progressive=True).all()

只出現progressive的結果

yt.streams.filter(progressive=True, subtype='mp4').all()

progressive & 格式為mp4

yt.streams.filter(only_audio=True, subtype='mp4').all()

只出現聲音 & 格式為mp4

order_by()

yt.streams.filter(progressive=True, subtype='mp4').order_by('resolution').desc().first()

可以搭配order_by()以及first()來選擇畫質(音質)最好的

yt.streams.filter(only_audio=True, subtype='mp4').order_by('abr').desc().first()

音質最好的mp4

畫質最好的progressive mp4

實作

檔案結構

|--flaTuber
    |--static
        |--css
            |--main.css
    |--templates
        |--base.html
        |--index.html
    |--venv
    app.py

建立表單class

from pytube import YouTube
from flask import Flask, render_template, flash, redirect, url_for
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired

app = Flask(__name__)
app.config['SECRET_KEY'] = "flaTuber"

class urlForm(FlaskForm):
	url = StringField(validators = [DataRequired()])
	video_submit = SubmitField('Video')
	audio_submit = SubmitField('Audio')

一個輸入欄位,兩顆submit按鈕

建立路由

@app.route('/', methods=['GET', 'POST'])
def index():
	form = urlForm()
	if form.validate_on_submit():
		pass
                # 待會補上
		flash('Done!')
		return redirect(url_for('index'))
	return render_template("index.html", form=form)

一個輸入欄位,兩顆submit按鈕

模板

<!DOCTYPE html>
<html lang="zh-TW">
<head>
    <link rel="icon" type="image/png" href="../static/../static/images/logo.png" />
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
    <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='css/main.css') }}">
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>{% block title %}{% endblock title %}</title>
</head>
<body class="bg-light">
    <div id="wrap">
        <div id="main" class="container d-flex flex-column">
			{% for message in get_flashed_messages() %}
			<div class="alert alert-success">
				<button type="button" class="close" data-dismiss="alert">&times;</button>
				{{ message }}
			</div>
			{% endfor %}
			
            {% block content %}
            {% endblock content %}
        </div>
    </div>
    <footer class="footer bg-secondary text-light">
        <p class="footer_text text-center pt-3">Created by 土豆</a></p>
    </footer>
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
</body>
</html>

templates/base.html

模板

{% extends "base.html" %}
{% block title %}FlaTuber{% endblock title %}
{% block content %}
    <form method="POST">
        <h1 class='page-title mt-5 text-primary' style="text-align: center; font-size: 3.5rem">Flatuber</h1>
        <div class="mt-5 form-group col-xl-5 col-md-6" style="margin: 15px auto">
            {{ form.hidden_tag() }}
            {{ form.url(placeholder="請輸入連結", class="form-control") }}
        </div>
	<div class="text-center">
	    {{ form.video_submit(class="btn btn-primary mx-1 bg-danger", id="btnVideo") }}
	    {{ form.audio_submit(class="btn btn-primary mx-1 bg-success", id="btnAudio") }}
	</div>
    </form>
{% endblock content %}

templates/index.html

模板

/* 避免sticky navbar 覆蓋到內容 */
body {
    padding-top: 70px;
}

body,input,button,select,textarea{
    font-family: "Helvetica Neue", Helvetica, Arial, "微軟正黑體", "微软雅黑", sans-serif;
}


/* 讓footer可以正常出現在下方 */
html, body {
    height: 100%;
}

#wrap {
    min-height: 100%;
}

#main {
    overflow: auto;
    padding-bottom: 50px;
}

.footer {
    position: relative;
    margin-top: -50px; /* negative value of footer height */
    height: 50px;
    clear:both;
  }

.footer_text{
    font-size: 0.9rem;
    margin: 0;
}
.sirla_link { 
    color: inherit; 
    text-decoration: underline;
}

static/css/main.css

主要邏輯

@app.route('/', methods=['GET', 'POST'])
def index():
	form = urlForm()
	if form.validate_on_submit():
		press_video = form.video_submit.data
		press_audio = form.audio_submit.data	
		url = form.url.data
		yt = YouTube(url)
		if press_video:
			video = yt.streams.filter(subtype='mp4', progressive=True).order_by('resolution').desc().first()
			video.download()
		elif press_audio:
			audio = yt.streams.filter(subtype='mp4', only_audio=True).order_by('abr').desc().first()
			audio.download()
		flash('Done!')
		return redirect(url_for('index'))
	return render_template("index.html", form=form)

讀取submit的data值,得知使用者按下哪顆按鈕

謝謝聆聽

15分鐘Falsk一個Youtube下載網站

By Sam Yang

15分鐘Falsk一個Youtube下載網站

  • 587