如何将 YouTube 视频的章节下载为单独的视频文件?

Lyr*_*Roy 10 video terminal youtube ffmpeg youtube-dl

一项新的 YouTube 功能

Youtube 最近启用了在 YouTube 视频中添加章节的选项。

在此处输入图片说明

这是通过在视频描述中简单地添加章节时间戳(开始时间)来完成的。

在此处输入图片说明

相关话题

使用 youtube-dl 我们可以

通过提供start time下载 YouTube 视频的一部分duration

将 YouTube 视频的描述提取.description文件。

是否有一个命令可以将 youtube 视频的章节下载为单独的视频文件而不提供start timeduration而是自动从 中提取时间戳.description


小智 13

你不能直接这样做。我编写了一个 Python 脚本,它将下载整个文件,处理描述文件,然后将下载的文件拆分为它的组件章节。

用法 - 如果将其保存到名为“yt_chapters.py”的文件中,则运行

yt_chapters.py --split "https://www.youtube.com/watch?v=m5lp8S-YgrQ"

您可以传递多个下载链接,它会遍历列表。

章节记录将添加到原始下载的 MP4 中。

要求:youtubedl、ffmpeg 和 MP4Box (GPAC)。如果您不想保留原始 MP4,或者您不关心它是否有章节,您可以注释掉该部分。youtubedl 和 ffmpeg 应该在您的系统路径中,否则更改下面的代码以包含可执行文件的完整路径。

#! /usr/bin/env python
# -*- coding: utf-8 -*-

import subprocess, os, argparse, datetime, re, sys

def get_source_name(download_source:str)->str:

    # Get the expected filename in order to process everything down the line
    result = subprocess.run(["youtube-dl", "--get-filename",
                            "-o", "%(title)s", download_source],
                            capture_output=True, encoding='UTF8')
    return result.stdout.rstrip('\n')

def get_best_mp4_single_file(download_source:str, description_file:str,
                             fix_required:bool=False)->list:

    # Download the file as mp4 (in order to use MP4)
    subprocess.run(["youtube-dl", "--write-description", "-f",
                    "bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4",
                    "-o", "%(title)s.%(ext)s", link_to_download])
    if fix_required:
        input("Fix the description file to have the chapters "
              "on their own lines in the format timecode name "
              "and then press Enter to continue...")
    return read_description_file(description_file=description_file)

def get_worst_mp4_single_file(download_source:str, description_file:str,
                              fix_required:bool=False)->list:

    # Download the file as mp4 (in order to use MP4)
    subprocess.run(["youtube-dl", "--write-description", "-f",
                    "worstvideo[ext=mp4]+worstaudio[ext=m4a]/mp4",
                    "-o", "%(title)s.%(ext)s", link_to_download])
    if fix_required:
        input("Fix the description file to have the chapters "
              "on their own lines in the format timecode name "
              "and then press Enter to continue...")
    return read_description_file(description_file=description_file)

def add_chapters_to_mp4(chapter_file_name:str, name_for_download:str)->None:

    # Use MP4Box to mux the chapter file with the mp4
    subprocess.run(["MP4Box", "-chap", chapter_file_name, name_for_download])

def read_description_file(description_file:str)->list:

    # Read the description file
    # Split into time and chapter name

    list_of_chapters = []

    with open(description_file, 'r') as f:
        # only increment chapter number on a chapter line
        # chapter lines start with timecode
        line_counter = 1
        for line in f:
            result = re.search(r"\(?(\d?[:]?\d+[:]\d+)\)?", line)
            try:
                # result = re.search("\(?(\d+[:]\d+[:]\d+)\)?", line)
                time_count = datetime.datetime.strptime(result.group(1), '%H:%M:%S')
            except:
                try:
                    # result = re.search("\(?(\d+[:]\d+)\)?", line)
                    time_count = datetime.datetime.strptime(result.group(1), '%M:%S')
                except:
                    continue
            chap_name = line.replace(result.group(0),"").rstrip(' :\n')
            chap_pos = datetime.datetime.strftime(time_count, '%H:%M:%S')
            list_of_chapters.append((str(line_counter).zfill(2), chap_pos, chap_name))
            line_counter += 1

    return list_of_chapters

def write_chapters_file(chapter_file:str, chapter_list:tuple)->None:

            #open(chapter_file, 'w').close()

            # Write out the chapter file based on simple MP4 format (OGM)
            with open(chapter_file, 'w') as fo:
                for current_chapter in chapter_list:
                    fo.write(f'CHAPTER{current_chapter[0]}='
                            f'{current_chapter[1]}\n'
                            f'CHAPTER{current_chapter[0]}NAME='
                            f'{current_chapter[2]}\n')


def split_mp4(chapters:list, download_filename:str, download_name:str)->None:

    # current_duration = subprocess.run(['ffprobe', '-i', download_filename,
                                       # '-show_entries', 'format=duration',
                                       # '-v', 'quiet', '-of', 'csv="p=0"'],
                                        # capture_output=True, encoding='UTF8')
    current_duration_pretext = subprocess.run(['ffprobe', '-i', download_filename,
                                       '-show_entries', 'format=duration',
                                       '-v', 'quiet'],
                                        capture_output=True, encoding='UTF8')
    current_duration = float(current_duration_pretext.stdout[18:-13])
    m, s = divmod(current_duration, 60)
    h, m = divmod(m, 60)
    current_dur = ':'.join([str(int(h)),str(int(m)),str(s)])
    for current_index, current_chapter in enumerate(chapters):
        # current_chapter will be a tuple: position, timecode, name
        next_index = current_index + 1
        start_time = current_chapter[1]
        try:
            end_time = chapters[next_index][1]
        except:
            end_time = current_dur
        output_name = f'{download_name} - ({current_chapter[2]}).mp4'
        subprocess.run(["ffmpeg", "-ss", start_time, "-to", end_time,
                        "-i", download_filename, "-acodec", "copy",
                        "-vcodec", "copy", output_name])

if __name__ == '__main__':

    parser = argparse.ArgumentParser(description='Download Youtube videos with chapters')

    parser.add_argument('links', metavar='N', type=str, nargs='+',
                        help='List of links to download')

    parser.add_argument("--split", help="Split chapters into individual files",
                        action="store_true")
    parser.add_argument("--test", help="Download worst version to speed up testing",
                        action="store_true")
    parser.add_argument("--fix", help="Pause to allow manual correction of the description file",
                        action="store_true")
    args = parser.parse_args()
    # print(args.links)

    for link_to_download in args.links:

        download_name = get_source_name(download_source=link_to_download)
        download_filename = f'{download_name}.mp4'
        description_file = f'{download_name}.description'
        chapter_file = f'{download_name}_chapter.txt'

        if not args.test:
            chapters = get_best_mp4_single_file(download_source=link_to_download,
                                                description_file=description_file,
                                                fix_required=args.fix)
        else:
            chapters = get_worst_mp4_single_file(download_source=link_to_download,
                                                description_file=description_file,
                                                fix_required=args.fix)

        
        if not chapter_file:
            print("No chapters found")
            sys.exit(1)
        
        write_chapters_file(chapter_file=chapter_file, chapter_list=chapters)

        if args.split:
            split_mp4(chapters=chapters, download_filename=download_filename,
                      download_name=download_name)
        
        add_chapters_to_mp4(chapter_file_name=chapter_file,
                              name_for_download=download_filename)

        # Clean up the text files
        os.remove(description_file)
        os.remove(chapter_file)
Run Code Online (Sandbox Code Playgroud)


Pab*_*o A 11

yt-dlp

不需要临时脚本。yt-dlpyoutube-dl 的一个分支,具有附加功能和修复,例如--split-chapters.

用法:

yt-dlp --split-chapters  [OPTIONS] URL [URL...]
Run Code Online (Sandbox Code Playgroud)

在 Linux 或 Mac 上安装brew install yt-dlp: .


相关如何使用 youtube-dl 列出 YouTube 视频中的章节标题