動画をダウンロードする

実現したこと

講義等の教育活動において、Youtubeなどの動画を使って説明したいことがあります。動画をローカルにダウンロードする様々なオンラインツールがありますが、広告が鬱陶しいのと複数の動画をダウンロードするのがまためんどくさいですね。Pythonで楽にするコードを書きましたので紹介します。

ただし、CCライセンスのものを選択するなど著作権回りは注意が必要です!!!また、授業目的での著作物の利用についても注意が必要なので、必ず確認してからにしてください💦

スクリプトの概要

1. CSVファイルやURLから動画URLを読み込む
• コマンドライン引数から、単一の動画URLまたはCSVファイルに含まれる複数の動画URLを指定します。
• CSVファイルを使用した場合、すべてのURLを順次処理します。
2. 動画のダウンロードを実行する
• 指定されたURL(もしくはCSVファイル内のURLリスト)から、YouTubeなどのサービスの動画をダウンロードします。
• 動画だけでなく、音声だけのダウンロードも可能です。
3. 開始時間と終了時間を指定してダウンロードする
• 動画全体ではなく、特定の部分(例えば、開始時間 00:02:00 から終了時間 00:05:00 まで)だけをダウンロードすることができます。
4. 進捗を表示し、処理後に結果を報告
• ダウンロードの進捗はプログレスバーで表示され、完了メッセージが表示されます。

環境構築

ffmpegをインストール

# For mac

brew install ffmpeg

# For ubuntu

sudo apt update && sudo apt install ffmpeg -y

Conda仮想環境構築

mkdir video_downloader
cd video_downloader
nano video_downloader.yml
# video_downloader.yml
name: video_downloader
channels:
  - defaults
  - conda-forge
dependencies:
  - python=3.12
  - yt-dlp
  - tqdm
conda env create -f video_downloader.yml
conda activate video_downloader

スクリプト

import argparse
import os
import yt_dlp
import csv
from tqdm import tqdm

class DownloadProgress:
    def __init__(self):
        self.pbar = None

    def download_hook(self, d):
        if d['status'] == 'downloading':
            if self.pbar is None:
                total = d.get('total_bytes') or d.get('total_bytes_estimate')
                self.pbar = tqdm(total=total, unit='B', unit_scale=True, desc=d['filename'])
            downloaded = d.get('downloaded_bytes', 0)
            self.pbar.update(downloaded - self.pbar.n)
        elif d['status'] == 'finished':
            if self.pbar is not None:
                self.pbar.close()
            print(f"Download completed. Converting...")

def download_media(url, quality, format, audio_only, audio_quality, output_dir, start_time, end_time):
    progress = DownloadProgress()
    if audio_only:
        ydl_opts = {
            'format': 'bestaudio/best',
            'postprocessors': [{
                'key': 'FFmpegExtractAudio',
                'preferredcodec': format,
                'preferredquality': str(audio_quality),
            }],
            'outtmpl': os.path.join(output_dir, '%(title)s.%(ext)s'),
            'quiet': True,
            'no_warnings': True,
            'progress_hooks': [progress.download_hook],
        }
    else:
        ydl_opts = {
            'format': f'{format}[height<={quality}]' if quality else f'{format}/bestvideo+bestaudio/best',
            'outtmpl': os.path.join(output_dir, '%(title)s.%(ext)s'),
            'quiet': True,
            'no_warnings': True,
            'progress_hooks': [progress.download_hook],
        }

    # Add time range options if specified
    if start_time or end_time:
        ydl_opts['download_ranges'] = download_range_func(start_time, end_time)
        ydl_opts['force_generic_extractor'] = True

    # Error handling options
    ydl_opts.update({
        'ignoreerrors': True,
        'no_color': True,
        'geo_bypass': True,
        'nocheckcertificate': True,
        'extractor_args': {'youtube': {'skip': ['dash', 'hls']}},
    })

    with yt_dlp.YoutubeDL(ydl_opts) as ydl:
        try:
            ydl.download([url])
            print(f"Media saved in {output_dir}")
        except Exception as e:
            print(f"An error occurred: {str(e)}")
            print("Trying alternative method...")
            try:
                ydl_opts['format'] = 'bestvideo+bestaudio/best'
                ydl.download([url])
                print(f"Media saved in {output_dir} using alternative method")
            except Exception as e:
                print(f"Alternative method also failed: {str(e)}")
                print("Please try updating yt-dlp or check if the video is available in your region.")

def download_range_func(start_time, end_time):
    start_seconds = time_to_seconds(start_time)
    end_seconds = time_to_seconds(end_time)

    def func(info_dict, ydl_obj):
        return [{
            'start_time': start_seconds,
            'end_time': end_seconds,
        }]
    return func

def time_to_seconds(time_str):
    if time_str:
        h, m, s = map(int, time_str.split(':'))
        return h * 3600 + m * 60 + s
    return None

def read_urls_from_csv(file_path):
    """Reads a CSV file and extracts URLs from it."""
    urls = []
    with open(file_path, newline='') as csvfile:
        reader = csv.reader(csvfile)
        for row in reader:
            if row:  # Check if row is not empty
                urls.append(row[0])  # Assuming URLs are in the first column
    return urls

def main():
    parser = argparse.ArgumentParser(description="Video/Audio Downloader")
    parser.add_argument("url", nargs='?', help="URL of the video (if not using --file)")
    parser.add_argument("-q", "--quality", type=int, default=1080, 
                        choices=[1080, 720, 480, 360],
                        help="Maximum video quality (height in pixels). "
                             "Choices are 1080 (default), 720, 480, or 360. "
                             "The highest available quality not exceeding "
                             "this value will be downloaded. "
                             "Ignored if --audio-only is used.")
    parser.add_argument("-f", "--format", default="mp4",
                        help="Desired media format (default: mp4). "
                             "For video: mp4, webm, mkv. "
                             "For audio (with --audio-only): mp3, m4a, wav, etc.")
    parser.add_argument("-o", "--output", default=".", 
                        help="Output directory (default: current directory)")
    parser.add_argument("--audio-only", action="store_true",
                        help="Download audio only")
    parser.add_argument("--audio-quality", type=int, default=192,
                        help="Audio bitrate in kbps (default: 192). "
                             "Common values: 128, 192, 256, 320. "
                             "Only applicable with --audio-only.")
    parser.add_argument("--start-time", type=str, 
                        help="Start time of the video (format: HH:MM:SS)")
    parser.add_argument("--end-time", type=str, 
                        help="End time of the video (format: HH:MM:SS)")
    parser.add_argument("--file", type=str, 
                        help="Path to a CSV file containing URLs of videos to download")

    args = parser.parse_args()

    if not os.path.exists(args.output):
        os.makedirs(args.output)

    if args.file:
        # If a file is specified, read URLs from the file and download each video
        urls = read_urls_from_csv(args.file)
        for url in urls:
            download_media(url, args.quality, args.format, args.audio_only, args.audio_quality, 
                           args.output, args.start_time, args.end_time)
    elif args.url:
        # If a URL is specified, download the single video
        download_media(args.url, args.quality, args.format, args.audio_only, args.audio_quality, 
                       args.output, args.start_time, args.end_time)
    else:
        print("Error: You must provide either a URL or a CSV file with the --file option.")
        parser.print_help()

if __name__ == "__main__":
    main()

Usage

python video_downloader.py --help
usage: video_downloader.py [-h] [-q {1080,720,480,360}] [-f FORMAT] [-o OUTPUT] [--audio-only] [--audio-quality AUDIO_QUALITY]
                           [--start-time START_TIME] [--end-time END_TIME] [--file FILE]
                           [url]

YouTube Video/Audio Downloader

positional arguments:
  url                   URL of the video (if not using --file)

options:
  -h, --help            show this help message and exit
  -q {1080,720,480,360}, --quality {1080,720,480,360}
                        Maximum video quality (height in pixels). Choices are 1080 (default), 720, 480, or 360. The highest
                        available quality not exceeding this value will be downloaded. Ignored if --audio-only is used.
  -f FORMAT, --format FORMAT
                        Desired media format (default: mp4). For video: mp4, webm, mkv. For audio (with --audio-only): mp3, m4a,
                        wav, etc.
  -o OUTPUT, --output OUTPUT
                        Output directory (default: current directory)
  --audio-only          Download audio only
  --audio-quality AUDIO_QUALITY
                        Audio bitrate in kbps (default: 192). Common values: 128, 192, 256, 320. Only applicable with --audio-only.
  --start-time START_TIME
                        Start time of the video (format: HH:MM:SS)
  --end-time END_TIME   End time of the video (format: HH:MM:SS)
  --file FILE           Path to a CSV file containing URLs of videos to download

Examples

基本的な使用法(デフォルト設定):
python video_downloader.py "https://www.XXX.com/ZZZ"

複数動画の一括ダウンロード:
python video_downloader.py --file xxx.csv

CSV file example
https://xxx.be/example1
https://xxx.be/example2
https://xxx.be/example3

品質を指定してダウンロード:
python video_downloader.py "https://www.XXX.com/ZZZ" -q 360

特定のフォーマットを指定してダウンロード:
python video_downloader.py "https://www.XXX.com/ZZZ" -f webm

品質とフォーマットを指定してダウンロード:
python video_downloader.py ”https://www.XXX.com/ZZZ” -q 480 -f mp4

特定のディレクトリにダウンロード:
python video_downloader.py ”https://www.XXX.com/ZZZ” -o ~/Downloads

音声のみをダウンロード:
python video_downloader.py ”https://www.XXX.com/ZZZ” --audio-only

音声のみを特定の品質とフォーマットでダウンロード:
python video_downloader.py ”https://www.XXX.com/ZZZ” --audio-only -f m4a --audio-quality 256

プレイリスト内の特定の動画をダウンロード:
python video_downloader.py ”https://www.XXX.com/ZZZ”

最低品質でダウンロード(データ節約):
python video_downloader.py ”https://www.XXX.com/ZZZ” -q 360

特定のファイル名でダウンロード:
python video_downloader.py ”https://www.XXX.com/ZZZ” -o "~/Videos/%(title)s-%(resolution)s.%(ext)s"

まとめ

これで、YouTubeなどから動画を効率的に一括ダウンロードできるようになりました。特定の時間範囲を指定して部分的にダウンロードする機能も備えており、CSVファイルを使えば大量の動画も一度に処理できます。

動画のダウンロードが自動化されることで、手作業でのダウンロードにかかる手間を削減でき、より効率的に動画の管理が可能になります。研究や趣味など、他の重要な活動に時間を割くことができるようになると嬉しいですね!

繰り返しになりますが、CCライセンスのものを選択するなど著作権回りは注意が必要です!!!また、授業目的での著作物の利用についても注意が必要なので、必ず確認してからにしてください💦

今後も便利なスクリプトやツールを紹介していきますので、ぜひお楽しみに!質問や改善点があれば、ぜひコメントでお知らせください。

いいなと思ったら応援しよう!