import tkinter as tk from tkinter import messagebox import customtkinter import os from PIL import Image from winotify import Notification, audio from yt_dlp import YoutubeDL import ffmpeg import pyperclip import threading import sys import json import re version = "0.2" builddate = "2024/2/18" appname = "which_online_video" lib_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "lib") thm_conf = os.path.isfile(lib_path + "\\thm.png") cpurl = pyperclip.paste() class Download(): def info(self, url): try: ytdl = YoutubeDL() info = ytdl.extract_info(url, download=False) self.title = info.get("title", "タイトルなし") thm_url = info.get("thumbnail", "") if thm_conf: os.remove(lib_path + "\\thm.png") thm = YoutubeDL({"outtmpl": lib_path + "\\thm.png"}) thm.download([thm_url]) self.url = url self.thm = lib_path + "\\thm.png" self.creator = info.get("uploader", "タイトルなし") self.title_re = re.sub(r'[\\/:*?"<>|]+', "", self.title) return {"video_title": self.title, "video_title_re": self.title_re, "video_creator": self.creator, "thm_path": self.thm} except: messagebox.showerror("ダウンロードエラー", "入力されたURLにはアクセスできませんでした。\nURLが有効なものであるか確認の上、再度お試しください。") return "url_error" def video(self, url, dir_path, video_title, video_title_re, quality): dl_option = { "outtmpl": dir_path + "\\" + video_title_re + ".mp4", "format": "bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4", } dlp = YoutubeDL(dl_option) print("動画をダウンロードしています") Notify().downloading(video_title, "video") try: dlp.download([url]) print("成功") return {"file_name": video_title_re + ".mp4", "file_path": dir_path + "\\" + video_title_re + ".mp4", "dir_path": dir_path, "video_title": video_title, "video_title_re": video_title_re} except: print("失敗") return "download_error" def audio(self, url, dir_path, video_title, video_title_re): dl_option = { 'outtmpl': dir_path + "\\" + video_title_re + ".m4a", 'format': 'bestaudio[ext=m4a]' } dlp = YoutubeDL(dl_option) print("音声をダウンロードしています") Notify().downloading(video_title, "audio") try: dlp.download([url]) print("成功") return {"file_name": url + ".m4a", "file_path": dir_path + "\\" + video_title_re + ".m4a", "dir_path": dir_path, "video_title": video_title, "video_title_re": video_title_re} except: print("失敗") return "download_error" class Convert(): def h264(self, file_path, dir_path, video_title, encoder, bitrate_add): dl_video_info = ffmpeg.probe(file_path) bitrate = float(dl_video_info["streams"][0]["bit_rate"]) vcodec = dl_video_info["streams"][0]["codec_name"] if vcodec == "vp9": dl_video_converted_path = dir_path + "\\" + video_title + "_h264.mp4" try: stream = ffmpeg.input(file_path) stream_convert = ffmpeg.output(stream, dl_video_converted_path, vcodec=encoder, video_bitrate=int(bitrate + bitrate * (bitrate_add - 1)), audio_bitrate="320k") stream_convert.run() return {"file_path": dl_video_converted_path} except: return "convert_error" else: return {"file_path": file_path} class Notify(): def downloading(self, video_title, mode): if mode == "video": self.dl_notify = Notification( app_id=appname, title="動画をダウンロード中...", msg=video_title, icon=lib_path + r"\thm.png" ) elif mode == "audio": self.dl_notify = Notification( app_id=appname, title="音声をダウンロード中...", msg=video_title, icon=lib_path + r"\thm.png" ) self.dl_notify.set_audio(audio.Default, loop=False) self.dl_notify.show() def download_success(self, video_title, file_path, dir_path): self.dl_success_notify = Notification( app_id=appname, title="ダウンロードが完了しました", msg=video_title, icon=lib_path + r"\thm.png" ) self.dl_success_notify.add_actions( label="ファイルを開く", launch=file_path ) self.dl_success_notify.add_actions( label="保存先を開く", launch=dir_path ) self.dl_success_notify.set_audio(audio.Reminder, loop=False) self.dl_success_notify.show() def info(self): self.info_notification = Notification( app_id=appname, title="バージョン情報", msg=f"{appname} バージョン {version}", icon=lib_path + r"\logo.png" ) self.info_notification.show() class App(customtkinter.CTk): def __init__(self): super().__init__() self.config_file = lib_path + "\\config.json" if not os.path.isfile(self.config_file): dummylist = { "dl_path": "", "convert": "1", "encoder": "h264", "bitrate_add": "1.0" } with open(self.config_file, "w") as list_write: json.dump(dummylist, list_write, indent=4) with open(self.config_file, "r") as list_read: self.config = json.load(list_read) self.title(appname) self.iconbitmap(r".\lib\logo.ico") self.geometry("640x360") self.resizable(height=False, width=False) self.grid_rowconfigure(0, weight=1) self.grid_columnconfigure(1, weight=1) self.logo_icon = customtkinter.CTkImage(Image.open( os.path.join(lib_path, "logo32.png")), size=(32, 32)) self.info_icon = customtkinter.CTkImage(Image.open( os.path.join(lib_path, "info32.png")), size=(32, 32)) self.setting_icon = customtkinter.CTkImage(Image.open( os.path.join(lib_path, "setting_32.png")), size=(32, 32)) if thm_conf: pass else: thm_dummy = Image.new("L", (640, 360)) thm_dummy.save(lib_path + "\\thm.png") self.thm_preview = customtkinter.CTkImage(Image.open( os.path.join(lib_path, "thm.png")), size=(160, 90)) self.title_font = customtkinter.CTkFont( family="meiryo", size=16, weight="bold") self.info_font = customtkinter.CTkFont(family="meiryo", size=14) self.ui_font = customtkinter.CTkFont(family="meiryo", size=12) self.button_font = customtkinter.CTkFont( family="meiryo", size=14, weight="bold") self.frame = customtkinter.CTkFrame(self, corner_radius=0) self.frame.grid(row=0, column=0, sticky="nsew") self.frame.grid_rowconfigure(4, weight=1) self.dl_frame_button = customtkinter.CTkButton(self.frame, width=32, height=36, border_spacing=4, image=self.logo_icon, text="", fg_color="transparent", hover_color=("gray70", "gray30"), command=self.frame_select_dl) self.dl_frame_button.grid(row=0, column=0, sticky="ew") self.info_button = customtkinter.CTkButton(self.frame, width=32, height=36, border_spacing=4, image=self.info_icon, text="", fg_color="transparent", hover_color=("gray70", "gray30"), command=self.app_info) self.info_button.grid(row=2, column=0, sticky="ew") self.setting_frame_button = customtkinter.CTkButton(self.frame, width=32, height=36, border_spacing=4, image=self.setting_icon, text="", fg_color="transparent", hover_color=("gray70", "gray30"), command=self.frame_select_setting) self.setting_frame_button.grid(row=1, column=0, sticky="ew") # dl_frame self.dl_frame = customtkinter.CTkFrame(self, fg_color="transparent") self.dl_frame.grid(row=0, column=1) self.dl_frame.grid_rowconfigure(0, weight=1) self.dl_frame.grid_columnconfigure(0, weight=1) self.dl_info_frame = customtkinter.CTkFrame( self.dl_frame, fg_color="transparent") self.dl_info_frame.grid(row=0, column=0, padx=2, pady=2) self.dl_info_frame.grid_columnconfigure(0, weight=2) self.dl_info_frame.grid_columnconfigure(1, weight=1) self.dl_info_label_frame = customtkinter.CTkFrame( self.dl_info_frame, fg_color="transparent") self.dl_info_label_frame.grid(row=0, column=0, padx=8, pady=0) self.dl_video_title = customtkinter.CTkLabel( self.dl_info_label_frame, text="URLを入力してください...", width=360, font=self.title_font, wraplength=360, anchor=tk.W) self.dl_video_title.grid(row=0, column=0, padx=2, pady=4) self.dl_creator_title = customtkinter.CTkLabel( self.dl_info_label_frame, text="クリップボードの内容が自動的にコピーされています。", width=360, font=self.info_font, wraplength=360, anchor=tk.W) self.dl_creator_title.grid(row=1, column=0, padx=2, pady=4) self.dl_video_thm = customtkinter.CTkLabel( self.dl_info_frame, image=self.thm_preview, text="", anchor=tk.NE) self.dl_video_thm.grid(row=0, column=1, padx=8, pady=0) self.dl_input_frame = customtkinter.CTkFrame( self.dl_frame, fg_color="transparent") self.dl_input_frame.grid(row=1, column=0, padx=16, pady=2) self.dl_input_frame.grid_rowconfigure(0, weight=1) self.dl_input_frame.grid_columnconfigure(0, weight=1) self.dl_url_textbox = customtkinter.CTkEntry( self.dl_input_frame, placeholder_text="URLを入力", width=428, font=self.ui_font) self.dl_url_textbox.grid(row=0, column=0, padx=8, pady=8) self.dl_url_textbox.insert(0, cpurl) self.dl_path_textbox = customtkinter.CTkEntry( self.dl_input_frame, placeholder_text="保存先を入力...", width=428, font=self.ui_font) self.dl_path_textbox.grid(row=1, column=0, padx=8, pady=8) self.dl_path_paste_button = customtkinter.CTkButton( self.dl_input_frame, text="URLを貼り付け", command=self.dl_path_paste, font=self.button_font) self.dl_path_paste_button.grid(row=0, column=1, padx=8, pady=8) self.dl_path_button = customtkinter.CTkButton( self.dl_input_frame, text="保存先を選択", command=self.dl_path_select, font=self.button_font) self.dl_path_button.grid(row=1, column=1, padx=8, pady=8) self.dl_path_textbox.insert(0, self.config["dl_path"].rstrip('\n')) self.dl_control_frame = customtkinter.CTkFrame( self.dl_frame, fg_color="transparent") self.dl_control_frame.grid(row=2, column=0, padx=16, pady=6) self.dl_control_frame.grid_rowconfigure(0, weight=1) self.dl_control_frame.grid_columnconfigure(0, weight=1) self.dl_audio_switch = customtkinter.CTkSwitch( self.dl_control_frame, text="音声を出力", width=120, font=self.ui_font) self.dl_audio_switch.grid(row=1, column=1, padx=4, pady=6) self.dl_video_switch = customtkinter.CTkSwitch( self.dl_control_frame, text="動画を出力", width=120, font=self.ui_font) self.dl_video_switch.grid(row=1, column=0, padx=4, pady=6) self.dl_video_switch.select() self.dl_special_dir_switch = customtkinter.CTkSwitch( self.dl_control_frame, text="SpeDir", width=120, font=self.ui_font) self.dl_special_dir_switch.grid(row=2, column=0, padx=4, pady=6) self.dl_acodec_select = customtkinter.CTkComboBox(self.dl_control_frame, width=140, values=[ "bestaudio", "opus", "vorbis", "aac", "mp3"], font=self.ui_font, dropdown_font=self.ui_font) self.dl_acodec_select.grid(row=1, column=2, padx=4, pady=6) self.dl_start_button = customtkinter.CTkButton( self.dl_control_frame, text="ダウンロード", command=self.dl_start_thread, font=self.button_font) self.dl_start_button.grid(row=2, column=3, padx=4, pady=6) # setting_frame self.setting_frame = customtkinter.CTkFrame(self, fg_color="transparent") self.setting_frame.grid_rowconfigure(0, weight=1) self.setting_frame.grid_columnconfigure(0, weight=1) self.setting_video_frame = customtkinter.CTkFrame(self.setting_frame, fg_color="transparent") self.setting_video_frame.grid(row=0, column=0) self.setting_video_title = customtkinter.CTkLabel(self.setting_video_frame, text="動画", width=400, font=self.title_font, wraplength=400, anchor=tk.W) self.setting_video_title.grid(row=0, column=0) self.setting_video_convert_title = customtkinter.CTkLabel(self.setting_video_frame, text="H264に変換する", width=400, font=self.info_font, wraplength=400, anchor=tk.W) self.setting_video_convert_title.grid(row=1, column=0) self.setting_video_convert_detail = customtkinter.CTkLabel(self.setting_video_frame, text="動画のコーデックがH264以外の場合、自動的に変換します。", width=400, font=self.ui_font, wraplength=400, anchor=tk.W) self.setting_video_convert_detail.grid(row=2, column=0) self.setting_video_convert_switch = customtkinter.CTkSwitch(self.setting_video_frame, text="", width=120, font=self.ui_font) self.setting_video_convert_switch.grid(row=1, column=1) self.setting_video_encoder_title = customtkinter.CTkLabel(self.setting_video_frame, text="エンコーダーの選択", width=400, font=self.info_font, wraplength=400, anchor=tk.W) self.setting_video_encoder_title.grid(row=3, column=0) self.setting_video_encoder_detail = customtkinter.CTkLabel(self.setting_video_frame, text="変換時に使用するエンコーダーを選択します。", width=400, font=self.ui_font, wraplength=400, anchor=tk.W) self.setting_video_encoder_detail.grid(row=4, column=0) self.setting_video_encoder_combobox = customtkinter.CTkComboBox(self.setting_video_frame, width=140, values=["h264", "h264_qsv", "h264_nvenc", "libx264"], font=self.ui_font, dropdown_font=self.ui_font) self.setting_video_encoder_combobox.grid(row=3, column=1) self.setting_video_bitrate_title = customtkinter.CTkLabel(self.setting_video_frame, text="ビットレートの上乗せ", width=400, font=self.info_font, wraplength=400, anchor=tk.W) self.setting_video_bitrate_title.grid(row=5, column=0) self.setting_video_bitrate_detail = customtkinter.CTkLabel(self.setting_video_frame, text="H264に変換後、元動画の画質を維持するためにビットレートを上げます。", width=400, font=self.ui_font, wraplength=400, anchor=tk.W) self.setting_video_bitrate_detail.grid(row=6, column=0) self.setting_video_bitrate_combobox = customtkinter.CTkComboBox(self.setting_video_frame, width=140, values=["1", "1.25", "1.5", "2"], font=self.ui_font, dropdown_font=self.ui_font) self.setting_video_bitrate_combobox.grid(row=5, column=1) if self.config["convert"] == "1": self.setting_video_convert_switch.select() else: self.setting_video_convert_switch.deselect() self.setting_video_encoder_combobox.set(self.config["encoder"]) self.setting_video_bitrate_combobox.set(self.config["bitrate_add"]) self.select_frame_name("dl_frame") def select_frame_name(self, name): self.dl_frame_button.configure( fg_color=("gray75", "gray25") if name == "dl_frame" else "transparent") self.setting_frame_button.configure( fg_color=("gray75", "gray25") if name == "setting_frame" else "transparent") if name == "dl_frame": self.dl_frame.grid(row=0, column=1, sticky="nsew") else: self.dl_frame.grid_forget() if name == "setting_frame": self.setting_frame.grid(row=0, column=1, sticky="nsew") else: self.setting_frame.grid_forget() def frame_select_dl(self): self.select_frame_name("dl_frame") def frame_select_setting(self): self.select_frame_name("setting_frame") def dl_path_select(self): dl_path = tk.filedialog.askdirectory( title="保存場所を選択...", initialdir="%HOMEPATH%/Videos") dl_path_win = dl_path.replace("/", "\\") self.dl_path_textbox.delete(0, tk.END) self.dl_path_textbox.insert(0, dl_path_win) self.config["dl_path"] = dl_path_win def dl_path_paste(self): cpurl = pyperclip.paste() self.dl_url_textbox.delete(0, tk.END) self.dl_url_textbox.insert(0, cpurl) # ダウンロード def dl_start_thread(self): if self.dl_video_switch.get() == 0 and self.dl_audio_switch.get() == 0: messagebox.showerror("ダウンロードエラー", "動画、音声のどちらをダウンロードするかを選択してください。") sys.exit() if self.dl_url_textbox.get() == "": messagebox.showerror( "ダウンロードエラー", "URLが入力されていません。\n有効なURLを入力してから、もう一度お試しください。") sys.exit() dl_thread = threading.Thread(target=self.dl_start) dl_thread.start() def dl_start(self): error = 0 self.dl_start_button.configure( text="ダウンロード中...", state="disabled", fg_color="gray") if self.dl_special_dir_switch.get() == 1: video_path = self.dl_path_textbox.get() + "\\" + "video" + "\\" + "source" audio_path = self.dl_path_textbox.get() + "\\" + "audio" + "\\" + "source" else: video_path = self.dl_path_textbox.get() audio_path = self.dl_path_textbox.get() self.ytdl = Download().info(self.dl_url_textbox.get()) if self.ytdl == "url_error": error = 1 else: self.thm_preview = customtkinter.CTkImage(Image.open(os.path.join(lib_path, "thm.png")), size=(160, 90)) self.dl_video_thm.configure(image=self.thm_preview) self.dl_video_title.configure(text=self.ytdl["video_title"]) self.dl_creator_title.configure(text=self.ytdl["video_creator"]) if self.dl_video_switch.get() == 1: print(self.ytdl["video_title_re"]) if os.path.isfile(video_path + "\\" + self.ytdl["video_title_re"] + ".mp4") or os.path.isfile(video_path + "\\" + self.ytdl["video_title_re"] + "_h264.mp4"): video_file_num_check = 0 video_file_num = 1 while video_file_num_check == 0: if os.path.isfile(video_path + "\\" + self.ytdl["video_title_re"] + "_" + str(video_file_num) + ".mp4") or os.path.isfile(video_path + "\\" + self.ytdl["video_title_re"] + "_" + str(video_file_num) + "_h264.mp4"): video_file_num = video_file_num + 1 else: video_file_num_check = 1 self.video_dl = Download().video(self.dl_url_textbox.get(), video_path, self.ytdl["video_title"], self.ytdl["video_title_re"] + "_" + str(video_file_num), "kayoko") else: self.video_dl = Download().video(self.dl_url_textbox.get(), video_path, self.ytdl["video_title"], self.ytdl["video_title_re"], "kayoko") if self.video_dl == "download_error": messagebox.showerror("ダウンロードエラー", "ダウンロードの段階でエラーが発生しました。動画はダウンロードされません。") error = 1 else: if self.setting_video_convert_switch.get() == 1: self.video_convert = Convert().h264(self.video_dl["file_path"], self.video_dl["dir_path"], self.video_dl["video_title_re"], self.setting_video_encoder_combobox.get(), float(self.setting_video_bitrate_combobox.get())) if self.video_convert == "convert_error": messagebox.showerror("ダウンロードエラー", "コーデックの変換の段階でエラーが発生しました。動画は変換されずにダウンロードされます。") error = 1 else: self.file_path = self.video_convert["file_path"] else: self.file_path = self.video_dl["file_path"] if self.dl_audio_switch.get() == 1: self.ytdl = Download().info(self.dl_url_textbox.get()) if self.ytdl == "url_error": error = 1 else: if os.path.isfile(audio_path + "\\" + self.ytdl["video_title_re"] + ".m4a"): audio_file_num_check = 0 audio_file_num = 1 while audio_file_num_check == 0: if os.path.isfile(audio_path + "\\" + self.ytdl["video_title_re"] + "_" + str(audio_file_num) + ".m4a"): audio_file_num = audio_file_num + 1 else: audio_file_num_check = 1 self.audio_dl = Download().audio(self.dl_url_textbox.get(), audio_path, self.ytdl["video_title"], self.ytdl["video_title_re"] + "_" + str(audio_file_num)) else: self.audio_dl = Download().audio(self.dl_url_textbox.get(), audio_path, self.ytdl["video_title"], self.ytdl["video_title_re"]) if self.audio_dl == "download_error": messagebox.showerror("ダウンロードエラー", "ダウンロードの段階でエラーが発生しました。音声はダウンロードされません。") else: if self.dl_video_switch.get() == 0: self.file_path = self.audio_dl["file_path"] if error == 0: Notify().download_success(self.ytdl["video_title"], self.file_path, self.dl_path_textbox.get()) self.dl_start_button.configure( text="ダウンロード", state="normal", fg_color="#3b8ed0") def app_info(self): Notify().info() def handler_close(self): self.config["dl_path"] = self.dl_path_textbox.get() self.config["convert"] = str(self.setting_video_convert_switch.get()) self.config["encoder"] = self.setting_video_encoder_combobox.get() self.config["bitrate_add"] = self.setting_video_bitrate_combobox.get() with open(self.config_file, "w") as list_write: json.dump(self.config, list_write, indent=4) self.destroy() if __name__ == "__main__": app = App() app.protocol("WM_DELETE_WINDOW", app.handler_close) app.mainloop()