diff options
| author | webmaster <webmaster@dangofactory.net> | 2026-03-11 11:06:26 +0900 |
|---|---|---|
| committer | webmaster <webmaster@dangofactory.net> | 2026-03-11 11:06:26 +0900 |
| commit | 021f6b03bec7a9aabdeed4490ea7bfe2925d5d33 (patch) | |
| tree | 6febc6a80404a495606dde89a5c97e09ff0b0475 | |
| download | yt-dlp-music-021f6b03bec7a9aabdeed4490ea7bfe2925d5d33.tar.gz yt-dlp-music-021f6b03bec7a9aabdeed4490ea7bfe2925d5d33.tar.bz2 yt-dlp-music-021f6b03bec7a9aabdeed4490ea7bfe2925d5d33.zip | |
init
| -rw-r--r-- | .gitignore | 3 | ||||
| -rw-r--r-- | Pipfile | 14 | ||||
| -rw-r--r-- | README.md | 90 | ||||
| -rwxr-xr-x | download_links_input.txt | 2 | ||||
| -rwxr-xr-x | requirements.txt | 3 | ||||
| -rwxr-xr-x | setting.ini | 86 | ||||
| -rwxr-xr-x | yt-dlp-music.py | 388 | ||||
| -rw-r--r-- | yt-dlp-music.spec | 38 |
8 files changed, 624 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0905e5c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +env
+build
+dist
@@ -0,0 +1,14 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +pyinstaller = "*" +yt_dlp = "*" + +[dev-packages] + +[requires] +python_version = "3.9" +python_full_version = "3.9.7" diff --git a/README.md b/README.md new file mode 100644 index 0000000..b2217bd --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ +## youtubeの動画をyt-dlpを使って音声としてダウンロードするスクリプト + +ダウンロードはこちらを実行してください。 +``` +git clone https://github.com/soburi59/yt-dlp-music.git +``` + + ### 準備: 依存関係の解決 +このスクリプトはyt-dlpとffmpeg(ffprobe)が必要です。 +デフォルトでは、exeファイルを使用するように設定されています。 +<br/> + +1. **yt-dlp** + +yt-dlpディレクトリに同梱済みです。 +ダウンロードする必要はありません。 + +2. **ffmpeg** + +以下のリンクからダウンロードするか、ビルドしてください。 +[https://ffmpeg.org/download.html](https://ffmpeg.org/download.html) + +解凍し、ffmpegディレクトリ以下に格納します。 +すでにffmpegがある場合は、setting.ini内の`[ffmpeg]` `exec`にパスを設定してください。(現在同梱を検討しています) + +<!-- 1. yt-dlp +以下のリンクからダウンロードしてください。 +[https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe) + +解凍し、yt-dlpディレクトリ以下に格納します。 +すでにyt-dlpがある場合は、setting.ini内の`[yt-dlp]` `exec`にパスを設定してください。 + + +2. ffmpeg +以下のリンクからダウンロードするか、ビルドしてください。 +[https://ffmpeg.org/download.html](https://ffmpeg.org/download.html) + +解凍し、ffmpegディレクトリ以下に格納します。 +すでにffmpegがある場合は、setting.ini内の`[ffmpeg]` `exec`をそのパスに設定してください。 --> + + +### 使用方法 +exeのショートカットを適当な場所に作成して実行します。 +ダウンロード先のディレクトリ、urlを順に指定してダウンロードします。 +download_links_input.txtにダウンロードしたい動画のurlを改行区切りで記載することで複数の動画をダウンロードすることもできます。 + +<hr> + +### 使用上の注意 + +> **Warning** +> youtubeに上がっているものをダウンロードするのはyoutubeの規約違反ですから自己責任です。 +> 違法アップロードをしてある動画をダウンロードするのは、私的利用に関わらず著作権法に抵触します。 +> 公式が上げている(アーティスト本人など)ものをダウンロードするのは規約違ですが法的に問題はありません。 +> 再配布は著作権法に抵触します。 +> 以上のことを踏まえて使用してください。 +<br/> + +> **Note** +> エラーによりダウンロードができない場合、setting.ini内の`auto-update`を`True`に変更するか、以下のコマンドを実行してください。 +> `yt-dlp.exe -U` +<br/> + +> **Note** +> このプログラムは、yt-dlpをexeとpythonからの呼び出しの両方から動作させる事が可能です。 +> また、このスクリプトのexe版にはyt-dlpがすでに組み込まれていますから、setting.iniの`[yt-dlp]` `type`および`[yt-dlp]` `options`を変更することで、pythonからの呼び出しに変更できます。(ffmpegは別途用意が必要です) + +<hr> + +#### 更新 +2023/4/5 自分の環境でしか動かなかったのでちゃんと動くようにしました ffmpegとyt-dlpのexeを入れています +2023/6/5 ありがたいことに改良してくれましたので https://github.com/FoxxCool/yt-dlp-music-fork を採用 +2023/6/5 少し変更(コンマでなく改行区切りで読み込むように,表示をわかりやすく,入力なしでmusic.txtを読み込むように) +2024/12/29 大幅に変更(設定ファイルをiniに変更,ダウンロードリンクのファイルをdownload_links_input.txtに変更) + +#### 詳細 +プログラムを改変した場合は以下を実行 +``` +pipenv install +pyinstaller --onefile yt-dlp-music.py +``` + +#### 謝辞 +以下のライブラリー・ソフトウェアを使用させていただきました。 +- yt-dlp +- mutagen +- pycryptodome +- FFmpeg及びffmpeg関連のコーデック + +この場を借りて感謝申し上げます。 diff --git a/download_links_input.txt b/download_links_input.txt new file mode 100755 index 0000000..d3d86f1 --- /dev/null +++ b/download_links_input.txt @@ -0,0 +1,2 @@ +https://www.youtube.com/watch?v=epqjaa2Uq28
+https://www.youtube.com/watch?v=JegrSN_Y-5w
diff --git a/requirements.txt b/requirements.txt new file mode 100755 index 0000000..e160e55 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +yt-dlp
+pycryptodome
+mutagen
diff --git a/setting.ini b/setting.ini new file mode 100755 index 0000000..65c465c --- /dev/null +++ b/setting.ini @@ -0,0 +1,86 @@ +; ================================================= +; +; YT-DLP-MUSIC - 設定ファイル +; 24/12/31 +; ================================================= + +; yt-dlp-musicに関する共通設定 +[common] +; ダウンロード先に指定する場所の初期値を設定します。 +; +path = ./out + +; ファイルの保存をする際ファイル名を決定するフォーマッタの設定します。 +; 参考資料: https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#output-template +; (注: エスケープする必要があります。) +; +formatter = %%(title)s.%%(ext)s + +; ダウンロード先を聞くか設定します。 +; +path-check = True + +; URLを指定するファイルの場所の初期値を設定します。 +; +input = download_links_input.txt + +; URLを聞くか設定します。 +; +input-check = True + +; ================================================= + +; 動画をダウンロードする際に使うyt-dlpについての設定 +[yt-dlp] +; yt-dlpについて、ライブラリーの種類を指定します。 +; +; 0 = yt-dlp.exe (Execution File) +; 1 = yt-dlp (Python Library) +type = 0 + +; yt-dlpについて、実行ファイルの場所を指定します。 +; (注: この設定はtypeを0に指定した場合のみ有効です) +; +exec = ./yt-dlp/yt-dlp.exe + +; yt-dlpについて、実行する際に指定するオプションを設定します。 +; 参考資料: https://github.com/yt-dlp/yt-dlp#usage-and-options +; https://github.com/yt-dlp/yt-dlp/blob/master/devscripts/cli_to_api.py +; (注: --format*, --extract-audio, --audio*, --ffmpeg-location, --outtmpl, については、設定しないようお願いします) +; null=None, true=True, false=False, "=' +; +; FIRST Option = For Execution Library +; SECOND Option = For Python Library +options = --progress --no-quiet --no-warnings --no-part --add-metadata --embed-thumbnail --convert-thumbnails jpg +;options = {"progress": true, "quiet": false, "no_warnings": true, "no_part": true, "writethumbnail": true, "postprocessors": [{"key": "FFmpegMetadata", "add_metadata": true, "when": "after_move"}, {"key": "EmbedThumbnail", "already_have_thumbnail": false, "when": "after_move"}, {"format": "jpg", "key": "FFmpegThumbnailsConvertor", "when": "before_dl"}]} + +; yt-dlpについて、起動時にアップデートを確認するかを設定します。 +; +; True = Enable auto update (Default) +; False = Disable auto update +auto-update = True + +; ================================================= + +; 動画を変換する際に使うffmpegについての設定 +[ffmpeg] +; ffmpegについて、実行ファイルの場所を指定します。 +; +exec = ./ffmpeg/ffmpeg.exe + +; ffmpegについて、yt-dlpによって出力されたファイルをどのような形式に変換するか設定します。 +; (例: mp3, wma, flac, wav, m4a, etc) +; +format = m4a + +; ffmpegについて、出力されるファイルのビットレートを指定します。 +; (注: 設定formatを設定する必要があります) +; +bitrate = 192 + +; プレイリストのダウンロード範囲設定 +[playlist] +; ダウンロードする開始番号と終了番号をハイフンで区切って指定します。 +; 例: 1-10 +; 0 = 全ての動画をダウンロード +range = 0 diff --git a/yt-dlp-music.py b/yt-dlp-music.py new file mode 100755 index 0000000..0807c3d --- /dev/null +++ b/yt-dlp-music.py @@ -0,0 +1,388 @@ +# -*- coding: utf-8 -*- +""" +yt-dlp-music: YouTube動画のダウンロードと音声抽出を行うツール +""" +from dataclasses import dataclass +from configparser import ConfigParser +from typing import Optional, Dict, Any, Tuple +import json +import os +import re +import subprocess + +DEFAULT_CONFIG = { + 'common': { + 'path': './out', + 'formatter': '%%(title)s.%%(ext)s', + 'path-check': 'true', + 'input': 'download_links_input.txt', + 'input-check': 'true' + }, + 'yt-dlp': { + 'type': 0, + 'exec': './yt-dlp/yt-dlp.exe', + 'options': '--progress --no-quiet --no-warnings --no-part --add-metadata --embed-thumbnail --convert-thumbnails jpg', + 'auto-update': 'true' + }, + 'ffmpeg': { + 'exec': './ffmpeg/ffmpeg.exe', + 'format': 'mp3', + 'bitrate': '192' + }, + 'playlist': { + 'range': '0' + } +} +URL_PATTEN = r'^[a-zA-Z][a-zA-Z\d+\-.]*://(?:\w+\.)?\w+\.\w+(/.*)?$' +FILE_PATTEN = r'^.*\.[A-Za-z]{2,4}$' + +class LogManager: + def __init__(self): + self._initial_messages() + + def _initial_messages(self): + messages = [ + 'yt-dlp-music Video Download Tool', + 'Copyright (C) 2023-2025 soburi. / test20140', + ' ', + 'Copyright (c) 2000-2025 FFmpeg', + 'Copyright (c) 2023 mutagen', + 'Copyright (c) 2024 pycryptodome', + ' ' + ] + for msg in messages: + self.info(msg) + + def debug(self, message: str) -> None: + if message and message.replace('\n', '\\n'): + if '[download]' in message: + clean_message = message.replace('[DEBUG] ', '') + # ダウンロードは1行で十分だから上書きで表示 + print(f"\r{clean_message}", end='', flush=True) + else: + clean_message = message.replace('[DEBUG] ', '') + print(clean_message) + + def info(self, message: str) -> None: + if message and message.replace('\n', '\\n'): + if '[download]' in message: + print(f"\r{message}", end='', flush=True) + else: + print(message) + + def warning(self, message: str) -> None: + if message and message.replace('\n', '\\n'): + print(f"[WARNING] {message}") + + def error(self, message: str) -> None: + if message and message.replace('\n', '\\n'): + print(f"[ERROR] {message}") + +@dataclass +class Movie: + """動画情報を保持するクラス。使うかもしれない""" + title: str = 'DELETED MOVIE' + url: str = '' + uploader: str = '' + duration: str = '0' + pl_url: Optional[str] = None + pl_title: Optional[str] = None + + @classmethod + def from_info(cls, info_dict: Dict[str, Any], playlist_info: Optional[Dict[str, Any]] = None) -> 'Movie': + if not info_dict: + return cls() + + return cls( + title=info_dict.get('title', 'DELETED MOVIE'), + url=info_dict.get('original_url', ''), + uploader=info_dict.get('uploader', ''), + duration=info_dict.get('duration_string', '0'), + pl_url=playlist_info.get('webpage_url') if playlist_info else None, + pl_title=playlist_info.get('title') if playlist_info else None + ) + +class YTDLPManager: + """yt-dlpの操作を管理するクラス""" + # TODO: オブジェクト指向でやると管理しやすいかもしれない + def __init__(self, config: str, ffmpeg_config: Dict[str, Any], type: int, exec: str): + self.options = config + self.type = int(type) + self.exec = exec + self._setup_options(ffmpeg_config) + if self.type == 1: + import yt_dlp + self.ydl = yt_dlp.YoutubeDL(self.options) + + def _setup_options(self, ffmpeg_config: Dict[str, Any]) -> None: + """yt-dlpのオプションを設定""" + if self.type == 0: + if ffmpeg_config.get('format'): + self.options += " --format ba" + self.options += " --format-sort abr,acodec" + self.options += " --format-sort-force" + self.options += " --extract-audio" + self.options += " --audio-format "+ffmpeg_config['format'] + self.options += " --audio-quality "+ffmpeg_config.get('bitrate', '192') + self.options += " --ffmpeg-location "+ffmpeg_config['exec'] + + elif self.type == 1: + self.options = json.loads(self.options) + if ffmpeg_config.get('format'): + self.options["format"] = "ba" + self.options["format_sort"] = ["abr", "acodec"] + self.options["format_sort_force"] = True + self.options.setdefault('postprocessors', []).extend([{ + 'key': 'FFmpegExtractAudio', + 'preferredcodec': ffmpeg_config['format'], + 'preferredquality': ffmpeg_config.get('bitrate', '192') + }]) + self.options.update({ + 'logger': log_manager, + 'ffmpeg_location': ffmpeg_config['exec'] + }) + + def download(self, url: str, playlist_range: Optional[Tuple[int, int]] = None) -> None: + """URLからダウンロードを実行""" + # TODO: Pipeを利用したダウンロード エラー発生時に一時ファイルを自動削除 + if self.type == 0: + options = self.options + if playlist_range: + start, end = playlist_range + options += f' --playlist-items {start}-{end}' + result = subprocess.Popen(self.exec +" "+ options +" "+ url, stdout=subprocess.PIPE, text=True) + while result.poll() == None: + line = result.stdout.readline() + log_manager.info(line.strip()) + result.wait() + if not result.returncode == 0: + log_manager.info("Error during download: except 0") + + elif self.type == 1: + options = self.options.copy() + if playlist_range: + start, end = playlist_range + options['playlist_items'] = f'{start}-{end}' + try: + self.ydl.download([url]) + except Exception as e: + log_manager.error(f"Error during download: {str(e)}") + + def is_playlist(self, url: str) -> bool: + """URLがプレイリストかどうかを判定""" + if "list=" in url: + return True + else: + return False + + def update(self) -> None: + """yt-dlpの更新を実行""" + code = 0 + try: + if self.type == 0: + result = subprocess.run([self.exec, "-U"], capture_output=True, text=True) + code = result.returncode + elif self.type == 1: + import yt_dlp + yt_dlp.update.Updater(self.ydl).update() + code = 0 + if code == 0: + log_manager.info(f"{'YT-DLP Update':20} Check {'':8} OK") + else: + log_manager.error(f"{'YT-DLP Update':20} Check {'':8} ERROR") + except Exception as e: + log_manager.warning(f"Error checking yt-dlp update: {str(e)}") + + def params(self, key, value): + if self.type == 0: + options = str(self.options) + start = options.find(key) + if start == -1: + self.options = options +" "+key+" "+value + return + end = options.find('--', start + len(key)) + if end == -1: + end = len(options) + self.options = options[:start-1] + options[end+1:] +" "+key+" "+value + + elif self.type == 1: + _s_neste(self.ydl.params, key, value) + self.ydl._parse_outtmpl() + + +def _s_neste(dic, keys, value): + key = keys[0] + if len(keys) == 1: + dic[key] = value + else: + if key not in dic: + dic[key] = {} + _s_neste(dic[key], keys[1:], value) + +def load_config(file_path: str) -> Dict[str, Any]: + """設定ファイルを読み込む""" + if not os.path.exists(file_path): + create_default_config(file_path) + + config = ConfigParser() + config.read(file_path, encoding='utf-8') + + return { + 'common': { + 'path': config.get('common', 'path', fallback=DEFAULT_CONFIG['common']['path']), + 'formatter': config.get('common', 'formatter', fallback=DEFAULT_CONFIG['common']['formatter']), + 'path-check': config.getboolean('common', 'path-check', fallback=DEFAULT_CONFIG['common']['path-check']), + 'input': config.get('common', 'input', fallback=DEFAULT_CONFIG['common']['input']), + 'input-check': config.getboolean('common', 'input-check', fallback=DEFAULT_CONFIG['common']['input-check']) + }, + 'yt-dlp': { + 'type': config.getint('yt-dlp', 'type', fallback=DEFAULT_CONFIG['yt-dlp']['type']), + 'exec': config.get('yt-dlp', 'exec', fallback=DEFAULT_CONFIG['yt-dlp']['exec']), + 'options': config.get('yt-dlp', 'options', fallback=DEFAULT_CONFIG['yt-dlp']['options']), + 'auto_update': config.getboolean('yt-dlp', 'auto-update', fallback=DEFAULT_CONFIG['yt-dlp']['auto-update'] == 'true') + }, + 'ffmpeg': { + 'exec': config.get('ffmpeg', 'exec', fallback=DEFAULT_CONFIG['ffmpeg']['exec']), + 'format': config.get('ffmpeg', 'format', fallback=DEFAULT_CONFIG['ffmpeg']['format']), + 'bitrate': config.get('ffmpeg', 'bitrate', fallback=DEFAULT_CONFIG['ffmpeg']['bitrate']) + }, + 'playlist': { + 'range': config.get('playlist', 'range', fallback=DEFAULT_CONFIG['playlist']['range']) + } + } + +def create_default_config(file_path: str) -> None: + """デフォルトの設定ファイルを作成""" + config = ConfigParser() + for section, settings in DEFAULT_CONFIG.items(): + config[section] = settings + with open(file_path, 'w', encoding='utf-8') as f: + config.write(f) + +def check_ffmpeg(ffmpeg_path: str) -> None: + """FFmpegの存在確認とバージョンチェック""" + try: + result = subprocess.run([ffmpeg_path, "-version"], capture_output=True, text=True) + if result.returncode == 0: + log_manager.info(f"{'FFmpeg':20} Check {'':8} OK") + else: + log_manager.error(f"{'FFmpeg':20} Check {'':8} ERROR") + except Exception as e: + log_manager.warning(f"Error checking FFmpeg: {str(e)}") + +def get_playlist_range(range_config: str) -> Optional[Tuple[int, int]]: + """プレイリストの範囲を取得""" + while True: + log_manager.info(f"Enter playlist range (e.g., 1-10, all videos for 0, Press 'Enter' for setting.ini→{range_config})") + range_input = input(f"Playlist range > ").strip() or range_config + + if range_input == '0' or range_input.lower() == 'all': + return None + + match = re.match(r'^(\d+)-(\d+)$', range_input) + if match: + start, end = map(int, match.groups()) + if start <= end: + return (start, end) + + log_manager.warning("Invalid range format. Please enter '0' for all videos or use format like '1-10'") + +def get_links_input(path: str) -> []: + """ファイルからurl一覧を取得""" + lst = [] + with open(path, 'r', encoding='utf-8') as f: + for line in f: + lst.append(line.strip()) + return lst + +def fls_con() -> None: + """コンソールのバッファ削除""" + if os.name == 'nt': + import msvcrt + while msvcrt.kbhit(): + msvcrt.getch() + elif os.name == 'posix': + import select + while select.select([sys.stdin.fileno()], [], [], 0.0)[0]: + os.read(sys.stdin.fileno(), 4096) + + +def main(): + # 設定ファイルの読み込み + config = load_config('setting.ini') + #log_manager.debug(f"Loaded configration file. {config}\n") + + # FFmpegの確認 + check_ffmpeg(config['ffmpeg']['exec']) + + # YT-DLPマネージャーの初期化 + ytdlp = YTDLPManager( + config['yt-dlp']['options'], + config['ffmpeg'], + config['yt-dlp']['type'], + config['yt-dlp']['exec'] + ) + + # YT-DLPの更新 + if config['yt-dlp']['auto_update']: + ytdlp.update() + + _path = config['common']['path'] + _url = config['common']['input'] + while True: + try: + fls_con() + # 対話 + if config['common']['path-check']: + log_manager.info(f"\nEnter directory location or Press Enter to use {_path}") + path = input("Location >").strip() or _path + if path.lower() == 'exit': + break + if config['common']['input-check']: + log_manager.info(f"\nEnter YouTube URL (playlist or video link) or Press Enter to use {_url}") + url = input("URL >").strip() or _url + + formatter = config['common']['formatter'] + lst = [] + # 整理 + if config['yt-dlp']['type'] == 0: + ytdlp.params('--output', os.path.join(path, formatter)) + elif config['yt-dlp']['type'] == 1: + outtmpl = { + 'default': os.path.join(path, formatter), + #'chapter': os.path.join(path, '%(title)s - %(section_number)03d %(section_title)s [%(id)s].%(ext)s') + } + ytdlp.params(['outtmpl'], outtmpl) + + if (re.match(FILE_PATTEN, url)): + lst = get_links_input(url) + elif (re.match(URL_PATTEN, url)): + lst = [url] + elif (url.lower() == 'exit'): + break + else: + log_manager.error("Invalid url/path") + continue + + for mov in lst: + if mov.lower() == 'exit': + break + if ytdlp.is_playlist(mov): + range_tuple = get_playlist_range(config['playlist']['range']) + ytdlp.download(mov, range_tuple) + else: + ytdlp.download(mov) + except KeyboardInterrupt: + log_manager.warning("Operation cancelled by user") + break + except: + log_manager.error(f"An error occurred") + import traceback + traceback.print_exc() + + log_manager.info("yt-dlp-music is shutting down") + +if __name__ == "__main__": + print("Please wait...\n") + log_manager = LogManager() + main() diff --git a/yt-dlp-music.spec b/yt-dlp-music.spec new file mode 100644 index 0000000..76010c6 --- /dev/null +++ b/yt-dlp-music.spec @@ -0,0 +1,38 @@ +# -*- mode: python ; coding: utf-8 -*-
+
+
+a = Analysis(
+ ['yt-dlp-music.py'],
+ pathex=[],
+ binaries=[],
+ datas=[],
+ hiddenimports=[],
+ hookspath=[],
+ hooksconfig={},
+ runtime_hooks=[],
+ excludes=[],
+ noarchive=False,
+ optimize=0,
+)
+pyz = PYZ(a.pure)
+
+exe = EXE(
+ pyz,
+ a.scripts,
+ a.binaries,
+ a.datas,
+ [],
+ name='yt-dlp-music',
+ debug=False,
+ bootloader_ignore_signals=False,
+ strip=False,
+ upx=True,
+ upx_exclude=[],
+ runtime_tmpdir=None,
+ console=True,
+ disable_windowed_traceback=False,
+ argv_emulation=False,
+ target_arch=None,
+ codesign_identity=None,
+ entitlements_file=None,
+)
|
