下載YouTube影片的Pytube程式庫有個下載播放清單影片的Playlist類別,本文將修改之前的YouTube影音下載程式碼,讓它下載播放清單中的全部影片。
取得YouTube播放清單網址
以Great Art Explained頻道為例,點擊其中一個播放清單:
瀏覽器將透過這個網址播放影片:
https://www.youtube.com/watch?v=T15Kv6dtYO0&list=PLjBkTEtM_Tw_cQi2PXrD9zSdFFsr4NQ5U
如果點擊右側播放清單裡的任一影片,上面的網址後面會加上一個代表索引編號的index參數。
所以,播放清單影片的網址格式如下,透過程式下載影片時,index參數可以省略,因為無論是否添加index參數,都能取得清單裡的所有影片的網址。
透過YouTube伺服器的playlist程式,也能取得指定的播放清單的影片,格式如下:
例如,把上面網址裡的list參數,剪貼到playlist網址後面,將能檢視清單中的所有影片:
https://www.youtube.com/playlist?list=PLjBkTEtM_Tw_cQi2PXrD9zSdFFsr4NQ5U
透過Pytube的Playlist類別取得「播放清單」的所有影片網址
Pytube程式庫的Playlist類別接收一個「播放清單」網址;底下敘述將建立一個名叫‘pl’的Playlist物件,透過pl物件的video_urls屬性,或直接存取pl物件,都能傳回列表(list)格式的影片網址清單。
Playlist類別物件(即pl物件)包含YouTube物件,因此底下敘述可下載播放清單的全部影片:
for video in p.videos: video.streams.first().download()
本文的程式僅利用Playlist取得影片清單的全部網址,執行下載和合併影音的程式,沿用之前的程式碼。
筆者將下載播放清單影片的Python程式檔命名成tube_list.py,它支援之前的所有命令行參數,如:-a(僅下載聲音)和-fhd(高畫質格式),並新增一個指定下載影片數量的-end參數。
例如,在終端機或命令提示字元中執行tube_list.py,它將下載指定播放清單當中的前三個高畫質影片:
python tube_list.py 播放清單網址 -fhd -end 3
tube_list.py程式檔修改自之前的tube.py檔,main()函式的改動內容如下:
check_urls()函式負責從播放清單中取出一個網址,交給download_media()函式進行下載:
def check_urls(): global args global file_index # 下載檔的列表索引 global download_count download_count = 1 if file_index < len(videos): vars(args)['url'] = videos[file_index] # 把影片網址傳給url參數 file_index = file_index + 1 print("下載影片:", vars(args)['url']) download_media(args)
download_media()函式將從args物件取得包含url(影片下載網址)在內的參數,所以上面的程式把下載影片的網址寫入args物件的url參數,這部份的相關說明請參閱「YouTube影片下載(一):合併視訊和音軌的Python程式」。
下載播放清單影片的完整Python程式碼
tube_list.py的完整程式碼如下:
# -*- coding: utf-8 -*- import argparse import os import platform from pytube import YouTube from pytube import Playlist import subprocess args = {} fileobj = {} download_count = 1 file_index = 0 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): 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(): vars(args)['a'] = False # 清空a參數 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('視訊和聲音合併完成') check_urls() 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('此影片有聲音,下載完畢!') check_urls() else: try: # 聲音檔重新命名 os.rename(file_path, os.path.join( fileobj['dir'], 'temp_audio.mp4')) except: print("聲音檔重新命名失敗") # 合併聲音檔 merge_media() def check_urls(): global args global file_index global download_count download_count = 1 if file_index < len(videos): vars(args)['url'] = videos[file_index] # 設定成url參數 file_index = file_index + 1 print("下載影片:", vars(args)['url']) download_media(args) def main(): global videos 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="僅下載聲音") parser.add_argument("-end", action="store", help="影音清單數量上限") args = parser.parse_args() try: pl = Playlist(args.url) if args.end: videos = pl.video_urls[:int(args.end)] else: videos = pl.video_urls except: print('下載影片時發生錯誤,請確認網路連線和YouTube網址無誤。') return # print('影片列表: ', videos) check_urls() if __name__ == '__main__': main()
執行這個程式檔時要留意,Windows的CLI介面,如PowerShell,不允許命令參數包含'&'字元;若YouTube影片網址包含'&'字元,執行時將會出現如下的錯誤訊息:
請把'&'字元用引號刮住,或改用上文提到的playlist網址就可以了: