巴哈自動排版太過死板,把我格式都吃光,所以程式碼顏色都不見了
要怪就怪巴哈
2022/10/05
更新了程式碼讓程式又可以運作,YT又改規則沒辦法
最近想到如果是下載演唱會之類的
那在結束直播時會可以下載,公開時間短暫,變為私人影片
改為私人就無法獲取下載網址
下載時改為私人後,也會遇到聲音抓不到影片網址,下載不到聲音檔的問題
也許考慮在下載影片時先把聲音網址存起來,再執行下載影片
下載聲音時再把儲存的網址來出來下載,應該可以順利下載到聲音供合併
不過有空再說了...
2022/07/28
今天又試了一次短影片
可以正常下載,之前明明不行的,看來YT把短影片弄得跟一般影片一樣了
目前沒有對應短影片,最近忙其他事情,沒空也沒心情弄這個
2022/02/07
已經完成webm下載合併的部分
幾乎沒有需要再加強的地方了
除了 youtube定時更新阻擋模組使用需要更新
google是真的很勤勞,約一個禮拜就會改一點東西,讓你原本模組不能使用
2022/01/28 更新
目前更新了解析度識別出webm功能
按鈕的部分增加對選擇BOX的繫結
程式的操作如下:
這是初版,內容大多底定了
新的有加上一些文字,錯誤捕捉等等
已知問題,這是針對抓取mp4設計
有些影片高畫質mp4檔最高只到1080p,有些2K影片只提供webm檔
如果要支援webm可能還要大幅擴充程式碼加上專門處理webm的部分
想到就頭痛 QQ ,所以可能會做也可能不做,畢竟mp4就很夠用了
做這個程式時發現的小知識,YT高畫質都是漸進式下載,也就是看一段載入一段,不會一次下載完
但720p幾乎都是自適應下載,也就是一次載完讓你慢慢看,所以如果不想要一直緩衝的話
選擇720p等他載完慢慢看就好
姑且放個執行檔供下載使用,使用 pyinstaller打包本體10MB
ffmpeg執行檔佔了其他空間
內容有執行檔與 ffmpeg(合併影片與音訊用),ffmpeg.exe可自己放更新的版本
預設下載位址是根目錄的download資料夾
使用會被微軟防毒擋,大概是使用呼叫控制臺合併影音的指令被認為有害
程式碼都在下方,有沒有毒自己斟酌吧
import pytube
import os
import subprocess
import datetime
from tkinter import *
from tkinter import filedialog
from tkinter import ttk
import threading
fileobj = {}
reslist = []
video_link = ""
resolution = ""
caption = str
video_type = str
type_res = {}
# 影片合併
def merge_media():
global fileobj, path,text, video_type
if (video_type == "video/webm"):
temp_video = os.path.join(path,"temp_video.webm")
temp_audio = os.path.join(path,"temp_audio.webm")
temp_videoout = os.path.join(path,"temp_videoout.mp4")
cmd = f'".\\ffmpeg\\bin\\ffmpeg.exe" -i "{temp_video}" -i "{temp_audio}" -map 0:v -map 1:a -c copy "{temp_videoout}"'
else :
temp_video = os.path.join(path,"temp_video.mp4")
temp_audio = os.path.join(path,"temp_audio.mp4")
temp_videoout = os.path.join(path,"temp_videoout.mp4")
# 編碼器目錄 -i 一個影片,一個音訊 -map 指定影片跟音訊順序 -c 編碼 copy為不重新編碼
cmd = f'".\\ffmpeg\\bin\\ffmpeg.exe" -i "{temp_video}" -i "{temp_audio}" -map 0:v -map 1:a -c copy "{temp_videoout}"'
try:
# 進行合併 shell開啟代表可直接輸入格式化一整行字串,默認False
subprocess.call(cmd, shell = True)
# 檔案重新命名
os.rename(temp_videoout, os.path.join(fileobj["dir"], fileobj["name"]))
os.remove(temp_video)
os.remove(temp_audio)
print("合併完成")
text.insert(END, "合併完成,已成功下載檔案\n")
text.see(END)
downloadbutton["state"] = NORMAL
analysisbutton["state"] = NORMAL
filepathbutton["state"] = NORMAL
downloadsubbutton["state"] = NORMAL
except:
print("合併失敗")
text.insert(END, "合併失敗\n")
text.see(END)
downloadbutton["state"] = NORMAL
analysisbutton["state"] = NORMAL
filepathbutton["state"] = NORMAL
downloadsubbutton["state"] = NORMAL
# 下載初始化時動作
def downloading(chunk, file_handler, bytes_remaining):
global text
# 依照 pytube.io的介紹
# 第一個變數為尚未寫入磁碟的數據(bytes)
# 第二個變數為緩衝寫入
# 第三個變數為與檔案總量的差值
total = chunk.filesize
percent = (total-bytes_remaining) / total * 100
print("下載中… {:05.2f}%".format(percent))
strper = "下載中… {:05.2f}%".format(percent)
text.insert(END, strper)
text.insert(END, "\n")
text.see(END)
# 下載結束時動作
def complete(stream, file_path):
# 依照pytube.io的介紹
# 第一個變數為下載檔案
# 第二個變數為檔案路徑(含檔名)
global fileobj, video_link, path,text, video_type
# fileobj為字典型態,作用為下載後儲存下載路徑與檔案名稱(含附檔名)
fileobj["name"] = os.path.basename(file_path)
fileobj["dir"] = os.path.dirname(file_path)
# pytube提供檢查有無音軌的功能
if (stream.includes_audio_track):
print("此檔案有音軌,不須合併,已完成下載")
text.insert(END, "此檔案有音軌,不須合併\n")
text.see(END)
downloadbutton["state"] = NORMAL
analysisbutton["state"] = NORMAL
filepathbutton["state"] = NORMAL
downloadsubbutton["state"] = NORMAL
else:
print("檔案沒有音軌,將下載音訊檔案進行合併")
text.insert(END, "檔案沒有音軌,將下載音訊檔案進行合併\n")
try:
# 檔案重新命名
# rename(原檔案名稱,新檔案名稱) 可用絕對路徑
# path.join(路徑,路徑/檔名)可連接路徑
if (video_type == "video/webm"):
os.rename(file_path, os.path.join(fileobj["dir"], "temp_video.webm"))
else:
os.rename(file_path, os.path.join(fileobj["dir"], "temp_video.mp4"))
except Exception as err:
print("重新命名失敗")
print(err)
text.insert(END, "重新命名失敗\n")
text.see(END)
downloadbutton["state"] = NORMAL
analysisbutton["state"] = NORMAL
filepathbutton["state"] = NORMAL
downloadsubbutton["state"] = NORMAL
return
# 下載音訊檔, pytube提供自動獲取高音質音軌函式
# 檔案地址延用,若影片檔案名稱更改失敗則維持原檔名且中斷程式
yt_audio = pytube.YouTube(video_link)
print("開始下載音訊檔")
text.insert(END, "開始下載音訊檔\n")
text.see(END)
try:
if (video_type == "video/webm"):
yt_audio.streams.filter(mime_type="audio/webm").last().download(path)
else :
yt_audio.streams.get_audio_only().download(path)
text.insert(END, "音訊檔下載成功\n")
text.see(END)
except:
text.insert(END, "音訊檔下載失敗\n")
text.see(END)
return
try:
# 對音訊檔重新命名
print("對音訊檔重新命名")
text.insert(END, "對音訊檔重新命名\n")
text.see(END)
if (video_type == "video/webm"):
os.rename(file_path,os.path.join(fileobj["dir"], "temp_audio.webm"))
else:
os.rename(file_path,os.path.join(fileobj["dir"], "temp_audio.mp4"))
except:
print("音訊檔重新命名失敗")
text.insert(END, "音訊檔重新命名失敗\n")
text.see(END)
downloadbutton["state"] = NORMAL
analysisbutton["state"] = NORMAL
filepathbutton["state"] = NORMAL
downloadsubbutton["state"] = NORMAL
return
# 執行合併
merge_media()
# 網址分析,分析標題與影片長度,也把網址帶入程式中
def url_analysis():
global video_link, reslist, resbox, resolution, captionbox, caption, video_type, type_res
refer_solution=["4320p","2160p","1440p","1080p","720p","480p","360p","240p","144p"]
video_link = url.get()
if (video_link==""):
print("請輸入網址")
text.insert(END, "請輸入網址\n")
text.see(END)
return
yt = pytube.YouTube(url. get())
video_title.set(yt.title)
timeleng = str(datetime.timedelta(seconds = yt.length))
video_len.set(timeleng)
# 使用for迴圈過濾影片的可用畫質
reslist = []
for n in refer_solution:
ytt = yt.streams.filter(type = "video", res = n)
if (ytt):
reslist.append(n)
type_res[n] = ytt.first().mime_type
resolutionList.set(reslist)
# 設定下拉式選單
resbox["values"] = reslist
# 設定第一個為預設值
# 設定解析度預設值的影片格式為影片格式的預設值 (video/mp4 or video/webm)
video_type = type_res[reslist[0]]
if (video_type == "video/webm"):
text.insert(END, f"目前選擇的解析度為 {reslist[0]:5},影片格式是 {video_type:11} \n")
text.see(END)
elif (video_type == "video/mp4"):
text.insert(END, f"目前選擇的解析度為 {reslist[0]:5},影片格式是 {video_type:11} \n")
text.see(END)
else:
video_type = yt.streams.filter(res = resbox.get(), subtype = "mp4").first().mime_type
if (video_type == None):
text.insert(END, "格式不支援,請選擇其他解析度\n")
text.see(END)
else:
text.insert(END, f"目前選擇的解析度為 {resbox.get():5},影片格式是 {video_type:11} \n")
text.see(END)
resbox.current(0)
resolution = resbox.get()
# 設定字幕下拉式選單
sub = yt.captions
sublist = []
for i in sub:
sublist.append(i.name)
try:
captionbox["values"] = sublist
captionbox.current(0)
print(sublist)
text.insert(END, "本影片有可用的cc字幕\n")
text.see(END)
except:
captionbox["values"] = ["", ""]
captionbox.current(0)
print("本影片沒有cc字幕可用")
text.insert(END, "本影片沒有cc字幕可用\n")
text.see(END)
caption = captionbox.get()
def comboboxselected(event):
global video_type, type_res
yt = pytube.YouTube(url. get())
video_type = type_res[resbox.get()]
if (video_type == "video/webm"):
text.insert(END, f"目前選擇的解析度為 {resbox.get():5},影片格式是 {video_type:11} \n")
text.see(END)
elif (video_type == "video/mp4"):
text.insert(END, f"目前選擇的解析度為 {resbox.get():5},影片格式是 {video_type:11} \n")
text.see(END)
else:
video_type = yt.streams.filter(res = resbox.get(), subtype = "mp4").first().mime_type
if (video_type == None):
text.insert(END, "格式不支援,請選擇其他解析度\n")
text.see(END)
else:
text.insert(END, f"目前選擇的解析度為 {resbox.get():5},影片格式是 {video_type:11} \n")
text.see(END)
# 清除文字BOX中的文字
def deltext():
text.delete("1.0", "end")
# 下載路徑選擇,也把選擇的路徑帶入程式中
def filepathset():
global path
pathset = filedialog.askdirectory()
file_path.set(pathset)
path = pathset
def downloadYT():
global video_link, path, resolution, video_type
try:
yt = pytube.YouTube(video_link,on_progress_callback = downloading, on_complete_callback = complete)
yt.streams.filter(mime_type = video_type, res = resolution).first().download(path)
except Exception as err:
print("下載影片失敗")
text.insert(END, "下載影片失敗\n")
text.see(END)
print(err)
downloadbutton["state"] = NORMAL
analysisbutton["state"] = NORMAL
filepathbutton["state"] = NORMAL
downloadsubbutton["state"] = NORMAL
return
def downloadYTsub():
global caption, video_link, path
ytsub = pytube.YouTube(video_link)
sub = ytsub.captions
try:
text.insert(END, "開始下載字幕\n")
print("開始下載字幕\n")
text.see(END)
for i in sub:
if (i.name == caption):
subcode = i.code
ytsub.captions[subcode].download(title = ytsub.title, output_path = path)
text.insert(END, "字幕下載成功\n")
print("字幕下載成功\n")
text.see(END)
except:
text.insert(END, "字幕下載失敗\n")
print("字幕下載失敗\n")
text.see(END)
downloadbutton["state"] = NORMAL
analysisbutton["state"] = NORMAL
filepathbutton["state"] = NORMAL
downloadsubbutton["state"] = NORMAL
# caption.py 有問題無法下載字幕,下列網址提供解決辦法,更換 caption.py 內容
# 影片下載的多執行緒啟動器
def threadstart():
global resolution, resbox
downloadbutton["state"] = DISABLED
analysisbutton["state"] = DISABLED
filepathbutton["state"] = DISABLED
downloadsubbutton["state"] = DISABLED
resolution = resbox.get()
text.insert(END, "開始下載\n")
text.see(END)
threading.Thread(target = downloadYT).start()
# 字幕下載的多執行緒啟動器
def subthreadstart():
global caption, captionbox
downloadbutton["state"] = DISABLED
analysisbutton["state"] = DISABLED
filepathbutton["state"] = DISABLED
downloadsubbutton["state"] = DISABLED
caption = captionbox.get()
threading.Thread(target = downloadYTsub).start()
# 預設下載路徑,檢查路徑是否存在,不存在則新增資料夾
path = (".\\download")
if not os.path.isdir(path):
os.mkdir(path)
#------------------------------------------------------------------------------------------
# 建立根視窗
window = Tk()
window.title("YOUTUBE 下載器")
window.geometry("720x320")
window.resizable(1,1)
#----------------------------------------------------------------------------------
# 右鍵複製貼上模組
def make_menu(w):
global the_menu
the_menu = Menu(w, tearoff = 0)
the_menu.add_command(label="剪下")
the_menu.add_command(label="複製")
the_menu.add_command(label="貼上")
def show_menu(e):
w = e.widget
the_menu.entryconfigure("剪下",
command = lambda: w.event_generate("<<Cut>>"))
the_menu.entryconfigure("複製",
command = lambda: w.event_generate("<<Copy>>"))
the_menu.entryconfigure("貼上",
command = lambda: w.event_generate("<<Paste>>"))
the_menu.tk.call("tk_popup", the_menu, e.x_root, e.y_root)
#----------------------------------------------------------------------------------
# tkinter的GUI介面中的變數,需要宣告且賦值得時候要用.set給予值,取用則要用.get取得內容
video_title = StringVar()
video_title.set("")
video_len = StringVar()
video_len.set("")
url = StringVar()
url.set("")
file_path = StringVar()
file_path.set(".\\download")
resolutionList = StringVar()
urlleb = Label(window, text = " 網址:")
urlEntry = Entry(window, width = 70, textvariable = url)
analysisbutton = Button(window, text="解析網址", width=15, command = url_analysis)
filepathleb = Label(window, text = "儲存位置:")
filepathEntry = Entry(window, width = 70, textvariable = file_path)
filepathbutton = Button(window, text = "更改下載目錄", width = 15, command = filepathset)
leb2 = Label(window, text = "影片標題:")
leb3 = Label(window, width = 70,textvariable = video_title, anchor = W)
leb4 = Label(window, text = "影片時長:")
leb5 = Label(window, width = 70, textvariable = video_len, anchor = W)
downloadbutton = Button(window, text = "下載影片", width = 15, command = threadstart)
leb6 = Label(window, text = "可用的影片解析度:")
leb7 = Label(window, textvariable = resolutionList)
resbox = ttk.Combobox(window, state = "readonly")
resbox.bind("<<ComboboxSelected>>", comboboxselected)
# 讓 urlEntry與右鍵選單模組做繫結
urlEntry.bind_class("Entry", "<Button-3><ButtonRelease-3>", show_menu)
make_menu(window)
text = Text(window, height=11, width=70)
scrollbar = Scrollbar(window)
scrollbar.config(command = text.yview)
text.config(yscrollcommand = scrollbar.set)
deltextbutton = Button(window, text = "清除文字", command = deltext)
captionbox = ttk.Combobox(window, state = "readonly")
bel8 = Label(window, text = "可用的字幕語言:")
downloadsubbutton = Button(window, text = "下載cc字幕", width = 15, command = subthreadstart)
urlleb.grid(row = 0, column = 0, rowspan = 2)
urlEntry.grid(row = 0, column = 1, rowspan = 2, columnspan = 2)
analysisbutton.grid(row = 0, column = 3, rowspan = 2)
filepathleb.grid(row = 2, column = 0)
filepathEntry.grid(row = 2, column = 1, columnspan = 2)
filepathbutton.grid(row = 2, column = 3)
leb2.grid(row = 3, column = 0)
leb3.grid(row = 3, column = 1, sticky = W, columnspan = 2)
leb4.grid(row = 4, column = 0)
leb5.grid(row = 4, column = 1, sticky = W, columnspan = 2)
downloadbutton.grid(row = 5, column = 3)
leb6.grid(row = 5, column = 0)
leb7.grid(row = 5, column = 1, sticky = W)
resbox.grid(row = 5, column = 2,sticky = E)
text.grid(row = 7, column = 1, columnspan = 2)
scrollbar.grid(row = 7, column = 3, sticky = S + W + N)
deltextbutton.grid(row = 8, column = 2, sticky = E)
captionbox.grid(row = 9, column = 2, sticky = E)
bel8.grid(row = 9, column = 0)
downloadsubbutton.grid(row = 9, column = 3)
# 讓視窗持續運作,直到按關閉
window.mainloop()
#------------------------------------------------------------------------------------------
參考網站:
delftstack (GUI部分很多指令從裡面查)
參考書籍:
Python最強入門邁向頂尖高手之路:王者歸來(第二版)全彩版