YouTube影片下載(五):PyTube3程式庫更新說明

前幾天我把下載YouTube影片的PyTube3程式庫更新到最新版:

pip install pytube3 --upgrade

結果之前寫的YouTube影片下載程式出現錯誤,不能用了。經修改幾個地方,程式即可正常運作,而且測試下載幾個影片,原本無法下載的音樂影片都能正常下載了。

修改的部分如下:

all()函式已不推薦使用(deprecated)。filter()方法將傳回符合篩選結果全部媒體格式,因此敘述後面不必加上all()。例如,篩選出全部1080p解析度的視訊:

yt.streams.filter(res=”1080p”).all()

在新版pytube3程式庫中寫成:

yt.streams.filter(res=”1080p”)

處理下載進度的onProgress回呼函式,原本接收4個參數,像這樣:

def onProgress(stream, chunk, file_handle, remaining):
    total = stream.filesize
	percent = (total - remaining) / total * 100
	print('下載中…{:05.2f}%'.format(percent), end='\r')

新版本變成3個參數,刪除了file_handle,所以程式改寫成:

def onProgress(stream, chunk, remaining):
    total = stream.filesize
	percent = (total - remaining) / total * 100
	print('下載中…{:05.2f}%'.format(percent), end='\r')

處理下載完成的onComplete回呼函式,原本第2個參數是檔案物件(file_handle),函式程式透過該物件的name屬性取得檔案路徑:

def onComplete(stream, file_handle):
    global download_count, fileobj
    fileobj['name'] = os.path.basename(file_handle.name)
    fileobj['dir'] = os.path.dirname(file_handle.name)
      :

新版本把「檔案物件」改成「檔案路徑」參數,所以程式改寫成:

def onComplete(stream, file_path):
    global download_count, fileobj
    fileobj['name'] = os.path.basename(file_path)
    fileobj['dir'] = os.path.dirname(file_path)
    print('\r')
      :

改寫之後的合併視訊和音軌的Python程式如下:

import argparse
import os
import platform
from pytube import YouTube
import subprocess

args = {}
fileobj = {}
download_count = 1


def pyTube_folder():
    sys = platform.system()
    home = os.path.expanduser('~')

    if sys == 'Windows':
        folder = os.path.join(home, 'Videos', 'PyTube')
    elif sys == 'Darwin':
        folder = os.path.join(home, 'Movies', 'PyTube')

    if not os.path.isdir(folder):  # 若'PyTube'資料夾不存在…
        os.mkdir(folder)        # 則新增資料夾

    return folder


def onProgress(stream, chunk, remains):
    total = stream.filesize
    percent = (total-remains) / total * 100
    print('下載中… {:05.2f}%'.format(percent), end='\r')

# 列舉可用的解析度


def video_res(yt):
    res_set = set()
    video_list = yt.streams.filter(type="video")
    for v in video_list:
        res_set.add(v.resolution)

    # 傳回解析度表列,例如:['720p', '480p', '360p', '240p', '144p']
    return sorted(res_set, reverse=True, key=lambda s: int(s[:-1]))


def download_media(args):
    try:
        yt = YouTube(args.url, on_progress_callback=onProgress,
                     on_complete_callback=onComplete)
    except:
        print('下載影片時發生錯誤,請確認網路連線和YouTube網址無誤。')
        return

    filter = yt.streams.filter

    if args.a:  # 只下載聲音
        target = filter(type="audio").first()
    elif args.fhd:
        target = filter(type="video", resolution="1080p").first()
    elif args.hd:
        target = filter(type="video", resolution="720p").first()
    elif args.sd:
        target = filter(type="video", resolution="480p").first()
    else:
        target = filter(type="video").first()

    if target is None:
        print('沒有您指定的解析度,可用的解析度如下:')
        res_list = video_res(yt)

        for i, res in enumerate(res_list):
            print('{}) {}'.format(i+1, res))

        val = input('請選擇(預設{}):'.format(res_list[0]))

        try:
            res = res_list[int(val)-1]
        except:
            res = res_list[0]

        print('您選擇的是 {} 。'.format(res))
        target = filter(type="video", resolution=res).first()

    # 開始下載
    target.download(output_path=pyTube_folder())


def check_media(filename):
    # r = subprocess.Popen(["ffprobe", filename],
    #                      stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    # out, err = r.communicate()
    # 上面的敘述可改寫成:
    # out = subprocess.check_output(["ffprobe", filename])
    # 或者:
    out = subprocess.run(["ffprobe", filename],
                         stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    if (out.stdout.decode('utf-8').rfind('Audio') == -1):
        return -1  # 沒有聲音
    else:
        return 1


def merge_media():
    temp_video = os.path.join(fileobj['dir'], 'temp_video.mp4')
    temp_audio = os.path.join(fileobj['dir'], 'temp_audio.mp4')
    temp_output = os.path.join(fileobj['dir'], 'output.mp4')

    cmd = f'ffmpeg -i {temp_video} -i {temp_audio} \
        -map 0:v -map 1:a -c copy -y {temp_output}'
    try:
        subprocess.run(cmd, shell=True)
        # 視訊檔重新命名
        os.rename(temp_output, os.path.join(fileobj['dir'], fileobj['name']))
        os.remove(temp_audio)
        os.remove(temp_video)
        print('視訊和聲音合併完成')
    except:
        print('視訊和聲音合併失敗')


def onComplete(stream, file_path):
    global download_count, fileobj
    fileobj['name'] = os.path.basename(file_path)
    fileobj['dir'] = os.path.dirname(file_path)
    print('\r')

    if download_count == 1:
        if check_media(file_path) == -1:
            print('此影片沒有聲音')
            download_count += 1
            try:
                # 視訊檔重新命名
                os.rename(file_path, os.path.join(
                    fileobj['dir'], 'temp_video.mp4'))
            except:
                print('視訊檔重新命名失敗')
                return

            print('準備下載聲音檔')
            vars(args)['a'] = True  # 設定成a參數
            download_media(args)    # 下載聲音
        else:
            print('此影片有聲音,下載完畢!')
    else:
        try:
            # 聲音檔重新命名
            os.rename(file_path, os.path.join(
                fileobj['dir'], 'temp_audio.mp4'))
        except:
            print("聲音檔重新命名失敗")
        # 合併聲音檔
        merge_media()


def main():
    global args
    parser = argparse.ArgumentParser()
    parser.add_argument("url", help="指定YouTube視訊網址")
    parser.add_argument("-sd", action="store_true", help="選擇普通(480P)畫質")
    parser.add_argument("-hd", action="store_true", help="選擇HD(720P)畫質")
    parser.add_argument("-fhd", action="store_true", help="選擇Full HD(1080P)畫質")
    parser.add_argument("-a", action="store_true", help="僅下載聲音")

    args = parser.parse_args()
    download_media(args)


if __name__ == '__main__':
    main()
Posts created 470

9 thoughts on “YouTube影片下載(五):PyTube3程式庫更新說明

  1. 你好,當以上程式運行完,出現以下訊息:
    下載中… 100.00%
    此影片沒有聲音
    準備下載聲音檔
    下載中… 100.00%
    ‘ffmpeg’ 不是內部或外部命令、可執行的程式或批次檔。
    視訊和聲音合併失敗
    請問該改寫程式中哪一部分?
    謝謝

    1. 你需要下載ffmpeg,然後在ffmpeg程式所在路徑執行此Python程式,或者像《超圖解Python程式設計入門》第5-2單元那樣,設定一個ffmpeg的路徑變數,或是在系統環境變數Path裡面,加入ffmpeg所在路徑(例如:”D:\Program Files\ffmpeg\bin”)。

      thanks,
      jeffrey

  2. 執行過後
    這行 vars(args)[‘a’] = True # 設定成a參數
    報錯 vars() argument must have __dict__ attribute

  3. 我原先問題已解決了 我是看著書上做的 我把global args 加進def main(): 然後書上寫的是download_video(yt, args) 所以我在第10行建立一個 yt={} 並把global yt 加進 def main() 然後 140行 改成 download_video(yt , args)就可以了 但我發現新的問題 第55行的 if args.a: 我把 args.a跟其他對調像是args.fhd 寫成 elif args.a:後 在下載音檔時 會變成下載影像 變成兩個影像檔在合併 進而合併失敗 請問這是為甚麼?

  4. 趙老師好,我今天在執行下載程式時遇到以下錯誤
    Traceback (most recent call last):
    File “E:\Coding\python\youtube.py”, line 188, in
    main()
    File “E:\Coding\python\youtube.py”, line 185, in main
    check_urls()
    File “E:\Coding\python\youtube.py”, line 154, in check_urls
    if file_index < len(videos):
    File "C:\Users\USER\AppData\Roaming\Python\Python310\site-packages\pytube\helpers.py", line 89, in __len__
    self.generate_all()
    File "C:\Users\USER\AppData\Roaming\Python\Python310\site-packages\pytube\helpers.py", line 105, in generate_all
    next_item = next(self.gen)
    File "C:\Users\USER\AppData\Roaming\Python\Python310\site-packages\pytube\contrib\playlist.py", line 281, in url_generator
    for page in self._paginate():
    File "C:\Users\USER\AppData\Roaming\Python\Python310\site-packages\pytube\contrib\playlist.py", line 118, in _paginate
    json.dumps(extract.initial_data(self.html))
    File "C:\Users\USER\AppData\Roaming\Python\Python310\site-packages\pytube\contrib\playlist.py", line 58, in html
    self._html = request.get(self.playlist_url)
    File "C:\Users\USER\AppData\Roaming\Python\Python310\site-packages\pytube\contrib\playlist.py", line 48, in playlist_url
    return f"https://www.youtube.com/playlist?list={self.playlist_id}"
    File "C:\Users\USER\AppData\Roaming\Python\Python310\site-packages\pytube\contrib\playlist.py", line 39, in playlist_id
    self._playlist_id = extract.playlist_id(self._input_url)
    File "C:\Users\USER\AppData\Roaming\Python\Python310\site-packages\pytube\extract.py", line 151, in playlist_id
    return parse_qs(parsed.query)['list'][0]
    KeyError: 'list'
    我不太懂Key Error是什麼類型的錯誤,請問要如何解決?
    另外VS CODE 的語法檢查說:
    Import "pytube" could not be resolved
    請問為什麼無法引用Pytube模組?

    1. 我在python 3.8.9虛擬環境中測試沒問題:

      pytube vs code

      我安裝的pytube是:

      pip install pytube

      而非pytube3,你可以執行這個命令先移除它再重裝:

      pip uninstall pytube3

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

Related Posts

Begin typing your search term above and press enter to search. Press ESC to cancel.

Back To Top