├── .gitignore
├── dlr_turns_30.mp4
├── manifest.csv
├── LICENSE
├── README.md
└── clipatron.py
/.gitignore:
--------------------------------------------------------------------------------
1 | dlr_turns_30
2 |
--------------------------------------------------------------------------------
/dlr_turns_30.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexwlchan/clipatron/master/dlr_turns_30.mp4
--------------------------------------------------------------------------------
/manifest.csv:
--------------------------------------------------------------------------------
1 | filename,start_time,duration,notes
2 | intro.mp4,00:00,00:03,animated "DLR 30"
3 | social_media.mp4,00:35.5,00:11,social media information
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2020 Alex Chan
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a
4 | copy of this software and associated documentation files (the "Software"),
5 | to deal in the Software without restriction, including without limitation
6 | the rights to use, copy, modify, merge, publish, distribute, sublicense,
7 | and/or sell copies of the Software, and to permit persons to whom the Software
8 | is furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
17 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
18 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
19 | OTHER DEALINGS IN THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # clipatron
2 |
3 | **This is a script for creating clips from a video file.**
4 | You create a spreadsheet that describes the clips you want to create, and then the script calls [`ffmpeg`](https://ffmpeg.org/) to create the clips.
5 |
6 | This is an example spreadsheet:
7 |
8 |
9 | | filename | start_time | duration | notes |
10 | | intro.mp4 | 00:00 | 00:03 | animated "DLR 30" |
11 | | social_media.mp4 | 00:35.5 | 00:11 | social media information |
12 |
13 |
14 | Combined with a video file, this would create two clips (`intro.mp4` and `social_media.mp4`).
15 |
16 | This is deliberately a simple script.
17 | If you need more complex options, you might be better off invoking ffmpeg directly, or looking at script like
18 |
19 | Original design and idea are from [@bessyboo](https://twitter.com/bessyboo)
20 |
21 |
22 |
23 | ## Installation
24 |
25 | You need two programs installed and in your PATH:
26 |
27 | - ffmpeg; see the [ffmpeg installation guide](https://www.ffmpeg.org/download.html).
28 | - Python; which you can download [from python.org](https://www.python.org/downloads/).
29 | Any recent version of Python should be fine (2.7 or 3.x).
30 |
31 | Then download this repository (click *"Clone or download"* > *"Download ZIP"*).
32 | Unzip the repository.
33 |
34 |
35 |
36 | ## Usage
37 |
38 | 1. Create a spreadsheet that describes the clips you want to create; one row per clip.
39 | This needs to have at least three named columns:
40 |
41 | * `filename` -- the name of the file you want to create for this clip
42 | * `start_time` -- where in the file the clip starts
43 | * `duration` -- how long the clip lasts
44 |
45 | You can add other columns if it's helpful (e.g. a `notes` column describing what's in the clip), but the script will ignore them.
46 |
47 | Save this spreadsheet as a CSV.
48 |
49 | 2. Open a terminal window, and navigate to the repository.
50 |
51 | ```console
52 | $ cd /path/to/repository
53 | ```
54 |
55 | 3. Run the script, passing (1) the path to the video file you want to clip and (2) the path to the CSV you created in step 1.
56 |
57 | The repository includes an example:
58 |
59 | ```console
60 | $ python clipatron.py --manifest manifest.csv --input dlr_turns_30.mp4
61 | ✨ Clipping done! ✨
62 | ```
63 |
64 |
65 |
66 | ## Example
67 |
68 | So you can see what the script expects, this repository includes an example [`manifest.csv`](manifest.csv) and video file.
69 |
70 | (This is a video from the [Transport for London YouTube channel](https://www.youtube.com/watch?v=6BoDePDGBHs).)
71 |
72 |
73 |
74 | ## Getting help
75 |
76 | If you get stuck, you can ask me for help [on Twitter](https://twitter.com/alexwlchan).
77 |
78 |
79 |
80 |
81 | ## License
82 |
83 | MIT.
84 |
--------------------------------------------------------------------------------
/clipatron.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- encoding: utf-8 -*-
3 | """
4 | This is a script for splitting video clips using ffmpeg.
5 |
6 | It takes a path to a video and a CSV file as input. The CSV must have
7 | at least three (named) columns:
8 |
9 | filename,start_time,duration
10 | scene_1.mp4,01:00,00:30 # creates a clip starting at 01:00, 30 seconds long
11 | scene_2.mp4,02:31,00:25 # creates a clip starting at 02:31, 25 seconds long
12 |
13 | Any extra columns in the CSV will be ignored.
14 |
15 | """
16 |
17 | from __future__ import print_function
18 |
19 | import argparse
20 | import subprocess
21 | import csv
22 | import errno
23 | import os
24 | import sys
25 |
26 |
27 | class ClipatronException(Exception):
28 | pass
29 |
30 |
31 | def parse_args(argv):
32 | """
33 | Read the command-line arguments.
34 | """
35 | parser = argparse.ArgumentParser(
36 | description="A script for clipping videos using ffmpeg."
37 | )
38 | parser.add_argument(
39 | "--input", dest="VIDEO", help="Path to the video file to clip", required=True
40 | )
41 | parser.add_argument(
42 | "--manifest",
43 | dest="CSV_MANIFEST",
44 | help="Path to the CSV file describing the clips to create",
45 | required=True,
46 | )
47 | return parser.parse_args(argv)
48 |
49 |
50 | def ffmpeg(*args):
51 | try:
52 | subprocess.check_call(["ffmpeg"] + list(args))
53 | except subprocess.CalledProcessError as err:
54 | print("Something went wrong: %r" % err, file=sys.stderr)
55 | print("Do you have ffmpeg installed?", file=sys.stderr)
56 | sys.exit(1)
57 |
58 |
59 | def get_rows(csv_manifest_path):
60 | """
61 | Read the rows from the CSV file
62 | """
63 | with open(csv_manifest_path) as infile:
64 | reader = csv.DictReader(infile)
65 |
66 | # Start at 2 because spreadsheet programs number their rows starting at 1,
67 | # and then the header is row 1
68 | for row_number, row in enumerate(reader, start=2):
69 | try:
70 | start_time = row["start_time"]
71 | duration = row["duration"]
72 | filename = row["filename"]
73 | except KeyError as err:
74 | print(
75 | "Row %d in your CSV is missing a required column: %s" %
76 | (row_number, err), file=sys.stderr
77 | )
78 | sys.exit(1)
79 |
80 | for (value, column_name) in [
81 | (start_time, "start_time"),
82 | (duration, "duration"),
83 | (filename, "filename")
84 | ]:
85 | if not value:
86 | print(
87 | "Row %d in your CSV has an empty value for %s!" %
88 | (row_number, column_name), file=sys.stderr
89 | )
90 | sys.exit(1)
91 |
92 | yield (start_time, duration, filename)
93 |
94 |
95 | def mkdir_p(path):
96 | """
97 | Create a directory if it doesn't already exist.
98 |
99 | From https://stackoverflow.com/a/600612/1558022
100 | """
101 | try:
102 | os.makedirs(path)
103 | except OSError as exc:
104 | if exc.errno == errno.EEXIST and os.path.isdir(path):
105 | pass
106 | else:
107 | raise
108 |
109 |
110 | if __name__ == "__main__":
111 | args = parse_args(sys.argv[1:])
112 |
113 | for (start_time, duration, filename) in get_rows(args.CSV_MANIFEST):
114 |
115 | # Should this video go in a standalone directory?
116 | dirname = os.path.dirname(filename)
117 | if not dirname:
118 | dirname = os.path.basename(os.path.splitext(args.VIDEO)[0])
119 |
120 | mkdir_p(dirname)
121 | out_path = os.path.join(dirname, os.path.basename(filename))
122 |
123 | ffmpeg(
124 | # See in the input file to `start_time`
125 | "-ss", start_time,
126 |
127 | # Read from `args.VIDEO`
128 | "-i", args.VIDEO,
129 |
130 | # Read at most `duration` from the video file
131 | "-t", duration,
132 |
133 | # Use the same codec as the original file
134 | "-vcodec", "copy",
135 |
136 | # Don't write any audio to the new file
137 | "-an",
138 |
139 | # Save the clip to `out_path`
140 | out_path
141 | )
142 |
143 | print("✨ Clipping done! ✨")
144 |
--------------------------------------------------------------------------------