Merge branch 'master' of github.com:Jab2870/dotfiles
This commit is contained in:
commit
82a7efd5d9
6 changed files with 707 additions and 5 deletions
|
@ -25,3 +25,5 @@ complete -o default -o nospace -W "$(/usr/bin/env ruby -ne 'puts $_.split(/[,\s]
|
|||
|
||||
#echo -e "Please don't sabotage my computer while I'm away \n\nTo turn on the print server, run the command 'cups'\n\nTo update 3d party plugins, run command 'u3p'" | /usr/bin/cowsay -f tux -W 80
|
||||
|
||||
|
||||
[ -r "$HOME/.smartcd_config" ] && ( [ -n $BASH_VERSION ] || [ -n $ZSH_VERSION ] ) && source ~/.smartcd_config
|
||||
|
|
696
bin/dvdrip
Executable file
696
bin/dvdrip
Executable file
|
@ -0,0 +1,696 @@
|
|||
#!/usr/bin/env python3
|
||||
# coding=utf-8
|
||||
|
||||
"""
|
||||
Rip DVDs quickly and easily from the commandline.
|
||||
|
||||
Features:
|
||||
- With minimal configuration:
|
||||
- Encodes videos in mkv files with h.264 video and aac audio.
|
||||
(compatible with a wide variety of media players without
|
||||
additional transcoding, including PS3, Roku, and most smart
|
||||
phones, smart TVs and tablets).
|
||||
- Preserves all audio tracks, all subtitle tracks, and chapter
|
||||
markers.
|
||||
- Intelligently chooses output filename based on a provided prefix.
|
||||
- Generates one video file per DVD title, or optionally one per
|
||||
chapter.
|
||||
- Easy to read "scan" mode tells you what you need need to know about
|
||||
a disk to decide on how to rip it.
|
||||
|
||||
Why I wrote this:
|
||||
This script exists because I wanted a simple way to back up DVDs with
|
||||
reasonably good compression and quality settings, and in a format I could
|
||||
play on the various media players I own including PS3, Roku, smart TVs,
|
||||
smartphones and tablets. Using mkv files with h.264 video and aac audio seems
|
||||
to be the best fit for these constraints.
|
||||
|
||||
I also wanted it to preserve as much as possible: chapter markers, subtitles,
|
||||
and (most of all) *all* of the audio tracks. My kids have a number of
|
||||
bilingual DVDs, and I wanted to back these up so they don't have to handle
|
||||
the physical disks, but can still watch their shows in either language. For
|
||||
some reason HandBrakeCLI doesn't have a simple “encode all audio tracks”
|
||||
option.
|
||||
|
||||
This script also tries to be smart about the output name. You just tell it
|
||||
the pathname prefix, eg: "/tmp/AwesomeVideo", and it'll decide whether to
|
||||
produce a single file, "/tmp/AwesomeVideo.mkv", or a directory
|
||||
"/tmp/AwesomeVideo/" which will contain separate files for each title,
|
||||
depending on whether you're ripping a single title or multiple titles.
|
||||
|
||||
|
||||
Using it, Step 1:
|
||||
|
||||
The first step is to scan your DVD and decide whether or not you want
|
||||
to split chapters. Here's an example of a disc with 6 episodes of a TV
|
||||
show, plus a "bump", all stored as a single title.
|
||||
|
||||
$ dvdrip --scan /dev/cdrom
|
||||
Reading from '/media/EXAMPLE1'
|
||||
Title 1/ 1: 02:25:33 720×576 4:3 25 fps
|
||||
audio 1: Chinese (5.1ch) [48000Hz, 448000bps]
|
||||
chapter 1: 00:24:15 ◖■■■■■■■■■‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥◗
|
||||
chapter 2: 00:24:15 ◖‥‥‥‥‥‥‥‥■■■■■■■■■‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥◗
|
||||
chapter 3: 00:24:14 ◖‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥■■■■■■■■■‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥◗
|
||||
chapter 4: 00:24:15 ◖‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥■■■■■■■■■■‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥◗
|
||||
chapter 5: 00:24:15 ◖‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥■■■■■■■■■‥‥‥‥‥‥‥‥◗
|
||||
chapter 6: 00:24:14 ◖‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥■■■■■■■■■◗
|
||||
chapter 7: 00:00:05 ◖‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥■◗
|
||||
|
||||
Knowing that this is 6 episodes of a TV show, I'd choose to split the
|
||||
chapters. If it was a movie with 6 chapters, I would choose to not
|
||||
split it.
|
||||
|
||||
Here's a disc with 3 2-segment episodes of a show, plus two "bumps",
|
||||
stored as 8 titles.
|
||||
|
||||
Reading from '/media/EXAMPLE2'
|
||||
Title 1/ 5: 00:23:22 720×576 4:3 25 fps
|
||||
audio 1: Chinese (2.0ch) [48000Hz, 192000bps]
|
||||
audio 2: English (2.0ch) [48000Hz, 192000bps]
|
||||
sub 1: English [(Bitmap)(VOBSUB)]
|
||||
chapter 1: 00:11:41 ◖■■■■■■■■■■■■■■■■■■■■■■■■■‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥◗
|
||||
chapter 2: 00:11:41 ◖‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥■■■■■■■■■■■■■■■■■■■■■■■■■■◗
|
||||
|
||||
Title 2/ 5: 00:22:40 720×576 4:3 25 fps
|
||||
audio 1: Chinese (2.0ch) [48000Hz, 192000bps]
|
||||
audio 2: English (2.0ch) [48000Hz, 192000bps]
|
||||
sub 1: English [(Bitmap)(VOBSUB)]
|
||||
chapter 1: 00:11:13 ◖■■■■■■■■■■■■■■■■■■■■■■■■‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥◗
|
||||
chapter 2: 00:11:28 ◖‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥■■■■■■■■■■■■■■■■■■■■■■■■■◗
|
||||
|
||||
Title 3/ 5: 00:22:55 720×576 4:3 25 fps
|
||||
audio 1: Chinese (2.0ch) [48000Hz, 192000bps]
|
||||
audio 2: English (2.0ch) [48000Hz, 192000bps]
|
||||
sub 1: English [(Bitmap)(VOBSUB)]
|
||||
chapter 1: 00:15:56 ◖■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥◗
|
||||
chapter 2: 00:06:59 ◖‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥■■■■■■■■■■■■■■■■◗
|
||||
|
||||
Title 4/ 5: 00:00:08 720×576 4:3 25 fps
|
||||
audio 1: English (2.0ch) [None]
|
||||
chapter 1: 00:00:08 ◖◗
|
||||
|
||||
Title 5/ 5: 00:00:05 720×576 4:3 25 fps
|
||||
chapter 1: 00:00:05 ◖◗
|
||||
|
||||
Given that these are 2-segment episodes (it's pretty common for kids'
|
||||
shows to have two segments per episode -- essentially 2 "mini-episodes") you
|
||||
can choose whether to do the default one video per title (episodes) or
|
||||
split by chapter (segments / mini-episodes).
|
||||
|
||||
Using it, Step 2:
|
||||
|
||||
If you've decided to split by chapter, execute:
|
||||
|
||||
dvdrip.py -c /dev/cdrom -o Output_Name
|
||||
|
||||
Otherwise, leave out the -c flag.
|
||||
|
||||
If there is only one video being ripped, it will be named Output_Name.mkv. If
|
||||
there are multiple files, they will be placed in a new directory called
|
||||
Output_Name.
|
||||
|
||||
Limitations:
|
||||
|
||||
This script has been tested on both Linux and Mac OS X with Python 3,
|
||||
HandBrakeCLI and VLC installed (and also MacPorts in the case of OS X).
|
||||
"""
|
||||
|
||||
# TODO: Detect if HandBrakeCLI is burning in vobsubs.
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
from pprint import pprint
|
||||
from collections import namedtuple
|
||||
from fractions import gcd
|
||||
|
||||
|
||||
class UserError(Exception):
|
||||
def __init__(self, message):
|
||||
self.message = message
|
||||
|
||||
CHAR_ENCODING = 'UTF-8'
|
||||
|
||||
def check_err(*popenargs, **kwargs):
|
||||
process = subprocess.Popen(stderr=subprocess.PIPE, *popenargs, **kwargs)
|
||||
_, stderr = process.communicate()
|
||||
retcode = process.poll()
|
||||
if retcode:
|
||||
cmd = kwargs.get("args")
|
||||
if cmd is None:
|
||||
cmd = popenargs[0]
|
||||
raise subprocess.CalledProcessError(retcode, cmd, output=stderr)
|
||||
return stderr.decode(CHAR_ENCODING, 'replace')
|
||||
|
||||
def check_output(*args, **kwargs):
|
||||
return subprocess.check_output(*args, **kwargs).decode(CHAR_ENCODING)
|
||||
|
||||
HANDBRAKE = 'HandBrakeCLI'
|
||||
|
||||
TITLE_COUNT_REGEXES = [
|
||||
re.compile(r'^Scanning title 1 of (\d+)\.\.\.$'),
|
||||
re.compile(r'^\[\d\d:\d\d:\d\d] scan: DVD has (\d+) title\(s\)$'),
|
||||
]
|
||||
|
||||
def FindTitleCount(scan, verbose):
|
||||
for regex in TITLE_COUNT_REGEXES:
|
||||
for line in scan:
|
||||
m = regex.match(line)
|
||||
if m: break
|
||||
if m:
|
||||
return int(m.group(1))
|
||||
if verbose:
|
||||
for line in scan:
|
||||
print(line)
|
||||
raise AssertionError("Can't find TITLE_COUNT_REGEX in scan")
|
||||
|
||||
|
||||
STRUCTURED_LINE_RE = re.compile(r'( *)\+ (([a-z0-9 ]+):)?(.*)')
|
||||
|
||||
def ExtractTitleScan(scan):
|
||||
result = []
|
||||
in_title_scan = False
|
||||
for line in scan:
|
||||
if not in_title_scan:
|
||||
if line.startswith('+'):
|
||||
in_title_scan = True
|
||||
if in_title_scan:
|
||||
m = STRUCTURED_LINE_RE.match(line)
|
||||
if m:
|
||||
result.append(line)
|
||||
else:
|
||||
break
|
||||
return tuple(result)
|
||||
|
||||
|
||||
TRACK_VALUE_RE = re.compile(r'(\d+), (.*)')
|
||||
|
||||
def MassageTrackData(node, key):
|
||||
if key in node:
|
||||
track_data = node[key]
|
||||
if type(track_data) is list:
|
||||
new_track_data = {}
|
||||
for track in track_data:
|
||||
k, v = TRACK_VALUE_RE.match(track).groups()
|
||||
new_track_data[k] = v
|
||||
node[key] = new_track_data
|
||||
|
||||
def ParseTitleScan(scan):
|
||||
pos, result = ParseTitleScanHelper(scan, pos=0, indent=0)
|
||||
|
||||
# HandBrakeCLI inexplicably uses a comma instead of a colon to
|
||||
# separate the track identifier from the track data in the "audio
|
||||
# tracks" and "subtitle tracks" nodes, so we "massage" these parsed
|
||||
# nodes to get a consistent parsed reperesentation.
|
||||
for value in result.values():
|
||||
MassageTrackData(value, 'audio tracks')
|
||||
MassageTrackData(value, 'subtitle tracks')
|
||||
return result
|
||||
|
||||
def ParseTitleScanHelper(scan, pos, indent):
|
||||
result = {}
|
||||
cruft = []
|
||||
while True:
|
||||
pos, node = ParseNode(scan, pos=pos, indent=indent)
|
||||
if node:
|
||||
if type(node) is tuple:
|
||||
k, v = node
|
||||
result[k] = v
|
||||
else:
|
||||
cruft.append(node)
|
||||
result[None] = cruft
|
||||
else:
|
||||
break
|
||||
if len(result) == 1 and None in result:
|
||||
result = result[None]
|
||||
return pos, result
|
||||
|
||||
def ParseNode(scan, pos, indent):
|
||||
if pos >= len(scan):
|
||||
return pos, None
|
||||
line = scan[pos]
|
||||
spaces, colon, name, value = STRUCTURED_LINE_RE.match(line).groups()
|
||||
spaces = len(spaces) / 2
|
||||
if spaces < indent:
|
||||
return pos, None
|
||||
assert spaces == indent, '%d <> %r' % (indent, line)
|
||||
pos += 1
|
||||
if colon:
|
||||
if value:
|
||||
node = (name, value)
|
||||
else:
|
||||
pos, children = ParseTitleScanHelper(scan, pos, indent + 1)
|
||||
node = (name, children)
|
||||
else:
|
||||
node = value
|
||||
return pos, node
|
||||
|
||||
def only(iterable):
|
||||
"""
|
||||
Return the one and only element in iterable.
|
||||
|
||||
Raises an ValueError if iterable does not have exactly one item.
|
||||
"""
|
||||
result, = iterable
|
||||
return result
|
||||
|
||||
Title = namedtuple('Title', ['number', 'info'])
|
||||
Task = namedtuple('Task', ['title', 'chapter'])
|
||||
|
||||
TOTAL_EJECT_SECONDS = 5
|
||||
EJECT_ATTEMPTS_PER_SECOND = 10
|
||||
|
||||
class DVD:
|
||||
def __init__(self, mountpoint, verbose, mount_timeout=0):
|
||||
if stat.S_ISBLK(os.stat(mountpoint).st_mode):
|
||||
mountpoint = FindMountPoint(mountpoint, mount_timeout)
|
||||
if not os.path.isdir(mountpoint):
|
||||
raise UserError('%r is not a directory' % mountpoint)
|
||||
self.mountpoint = mountpoint
|
||||
self.verbose = verbose
|
||||
|
||||
def RipTitle(self, task, output, dry_run, verbose):
|
||||
if verbose:
|
||||
print('Title Scan:')
|
||||
pprint(task.title.info)
|
||||
print('-' * 78)
|
||||
|
||||
audio_tracks = task.title.info['audio tracks'].keys()
|
||||
audio_encoders = ['faac'] * len(audio_tracks)
|
||||
subtitles = task.title.info['subtitle tracks'].keys()
|
||||
|
||||
args = [
|
||||
HANDBRAKE,
|
||||
'--title', str(task.title.number),
|
||||
'--preset', "High Profile",
|
||||
'--encoder', 'x264',
|
||||
'--audio', ','.join(audio_tracks),
|
||||
'--aencoder', ','.join(audio_encoders),
|
||||
]
|
||||
if task.chapter is not None:
|
||||
args += [
|
||||
'--chapters', str(task.chapter),
|
||||
]
|
||||
if subtitles:
|
||||
args += [
|
||||
'--subtitle', ','.join(subtitles),
|
||||
]
|
||||
args += [
|
||||
'--markers',
|
||||
'--optimize',
|
||||
#'--no-dvdnav', # TODO: turn this on as a fallback
|
||||
'--input', self.mountpoint,
|
||||
'--output', output,
|
||||
]
|
||||
if verbose:
|
||||
print(' '.join(('\n ' + a)
|
||||
if a.startswith('-') else a for a in args))
|
||||
print('-' * 78)
|
||||
if not dry_run:
|
||||
if verbose:
|
||||
subprocess.call(args)
|
||||
else:
|
||||
check_err(args)
|
||||
|
||||
def ScanTitle(self, i):
|
||||
for line in check_err([
|
||||
HANDBRAKE,
|
||||
#'--no-dvdnav', # TODO: turn this on as a fallback
|
||||
'--scan',
|
||||
'--title', str(i),
|
||||
'-i',
|
||||
self.mountpoint], stdout=subprocess.PIPE).split('\n'):
|
||||
if self.verbose:
|
||||
print('< %s' % line.rstrip())
|
||||
yield line
|
||||
|
||||
def ScanTitles(self, title_numbers, verbose):
|
||||
"""
|
||||
Returns an iterable of parsed titles.
|
||||
"""
|
||||
first = title_numbers[0] if title_numbers else 1
|
||||
raw_scan = tuple(self.ScanTitle(first))
|
||||
title_count = FindTitleCount(raw_scan, verbose)
|
||||
print('Disc claims to have %d titles.' % title_count)
|
||||
title_name, title_info = only(
|
||||
ParseTitleScan(ExtractTitleScan(raw_scan)).items())
|
||||
del raw_scan
|
||||
|
||||
def MakeTitle(name, number, info):
|
||||
assert ('title %d' % number) == name
|
||||
info['duration'] = ExtractDuration('duration ' + info['duration'])
|
||||
return Title(number, info)
|
||||
|
||||
yield MakeTitle(title_name, first, title_info)
|
||||
|
||||
to_scan = [x for x in range(1, title_count + 1)
|
||||
if x != first
|
||||
and ((not title_numbers)
|
||||
or x in title_numbers)]
|
||||
for i in to_scan:
|
||||
try:
|
||||
scan = ExtractTitleScan(self.ScanTitle(i))
|
||||
except subprocess.CalledProcessError as exc:
|
||||
warn("Cannot scan title %d." % i)
|
||||
else:
|
||||
title_info_names = ParseTitleScan(scan).items()
|
||||
if title_info_names:
|
||||
title_name, title_info = only(title_info_names)
|
||||
yield MakeTitle(title_name, i, title_info)
|
||||
else:
|
||||
warn("Cannot parse scan of title %d." % i)
|
||||
|
||||
def Eject(self):
|
||||
# TODO: this should really be a while loop that terminates once a
|
||||
# deadline is met.
|
||||
for i in range(TOTAL_EJECT_SECONDS * EJECT_ATTEMPTS_PER_SECOND):
|
||||
if not subprocess.call(['eject', self.mountpoint]):
|
||||
return
|
||||
time.sleep(1.0 / EJECT_ATTEMPTS_PER_SECOND)
|
||||
|
||||
def ParseDuration(s):
|
||||
result = 0
|
||||
for field in s.strip().split(':'):
|
||||
result *= 60
|
||||
result += int(field)
|
||||
return result
|
||||
|
||||
def FindMountPoint(dev, timeout):
|
||||
regex = re.compile(r'^' + re.escape(os.path.realpath(dev)) + r'\b')
|
||||
|
||||
now = time.time()
|
||||
end_time = now + timeout
|
||||
while end_time >= now:
|
||||
for line in check_output(['df', '-P']).split('\n'):
|
||||
m = regex.match(line)
|
||||
if m:
|
||||
line = line.split(None, 5)
|
||||
if len(line) > 1:
|
||||
return line[-1]
|
||||
time.sleep(0.1)
|
||||
now = time.time()
|
||||
raise UserError('%r not mounted.' % dev)
|
||||
|
||||
def FindMainFeature(titles, verbose=False):
|
||||
if verbose:
|
||||
print('Attempting to determine main feature of %d titles...'
|
||||
% len(titles))
|
||||
main_feature = max(titles,
|
||||
key=lambda title: ParseDuration(title.info['duration']))
|
||||
if verbose:
|
||||
print('Selected %r as main feature.' % main_feature.number)
|
||||
print()
|
||||
|
||||
def ConstructTasks(titles, chapter_split):
|
||||
for title in titles:
|
||||
num_chapters = len(title.info['chapters'])
|
||||
if chapter_split and num_chapters > 1:
|
||||
for chapter in range(1, num_chapters + 1):
|
||||
yield Task(title, chapter)
|
||||
else:
|
||||
yield Task(title, None)
|
||||
|
||||
def TaskFilenames(tasks, output, dry_run=False):
|
||||
if (len(tasks) > 1):
|
||||
def ComputeFileName(task):
|
||||
if task.chapter is None:
|
||||
return os.path.join(output,
|
||||
'Title%02d.mkv' % task.title.number)
|
||||
else:
|
||||
return os.path.join(output,
|
||||
'Title%02d_%02d.mkv'
|
||||
% (task.title.number, task.chapter))
|
||||
if not dry_run:
|
||||
os.makedirs(output)
|
||||
else:
|
||||
def ComputeFileName(task):
|
||||
return '%s.mkv' % output
|
||||
result = [ComputeFileName(task) for task in tasks]
|
||||
if len(set(result)) != len(result):
|
||||
raise UserError("multiple tasks use same filename")
|
||||
return result
|
||||
|
||||
def PerformTasks(dvd, tasks, title_count, filenames,
|
||||
dry_run=False, verbose=False):
|
||||
for task, filename in zip(tasks, filenames):
|
||||
print('=' * 78)
|
||||
if task.chapter is None:
|
||||
print('Title %s / %s => %r'
|
||||
% (task.title.number, title_count, filename))
|
||||
else:
|
||||
num_chapters = len(task.title.info['chapters'])
|
||||
print('Title %s / %s , Chapter %s / %s=> %r'
|
||||
% (task.title.number, title_count, task.chapter,
|
||||
num_chapters, filename))
|
||||
print('-' * 78)
|
||||
dvd.RipTitle(task, filename, dry_run, verbose)
|
||||
|
||||
Size = namedtuple('Size',
|
||||
['width', 'height', 'pix_aspect_width', 'pix_aspect_height', 'fps'])
|
||||
|
||||
SIZE_REGEX = re.compile(
|
||||
r'^\s*(\d+)x(\d+),\s*'
|
||||
r'pixel aspect: (\d+)/(\d+),\s*'
|
||||
r'display aspect: (?:\d+(?:\.\d+)),\s*'
|
||||
r'(\d+(?:\.\d+)) fps\s*$')
|
||||
|
||||
SIZE_CTORS = [int] * 4 + [float]
|
||||
|
||||
def ParseSize(s):
|
||||
return Size(*(f(x)
|
||||
for f, x in zip(SIZE_CTORS, SIZE_REGEX.match(s).groups())))
|
||||
|
||||
def ComputeAspectRatio(size):
|
||||
w = size.width * size.pix_aspect_width
|
||||
h = size.height * size.pix_aspect_height
|
||||
d = gcd(w, h)
|
||||
return (w // d, h // d)
|
||||
|
||||
DURATION_REGEX = re.compile(
|
||||
r'^(?:.*,)?\s*duration\s+(\d\d):(\d\d):(\d\d)\s*(?:,.*)?$')
|
||||
|
||||
class Duration(namedtuple('Duration', 'hours minutes seconds')):
|
||||
def __str__(self):
|
||||
return '%02d:%02d:%02d' % (self)
|
||||
|
||||
def in_seconds(self):
|
||||
return 60 * (60 * self.hours + self.minutes) + self.seconds
|
||||
|
||||
def ExtractDuration(s):
|
||||
return Duration(*map(int, DURATION_REGEX.match(s).groups()))
|
||||
|
||||
Chapter = namedtuple('Chapter', 'number duration')
|
||||
|
||||
def ParseChapters(d):
|
||||
"""
|
||||
Parses dictionary of (str) chapter numbers to chapter.
|
||||
|
||||
Result will be an iterable of Chapter objects, sorted by number.
|
||||
"""
|
||||
for number, info in sorted(((int(n), info) for (n, info) in d.items())):
|
||||
yield Chapter(number, ExtractDuration(info))
|
||||
|
||||
AUDIO_TRACK_REGEX = re.compile(
|
||||
r'^(\S+)\s*((?:\([^)]*\)\s*)*)(?:,\s*(.*))?$')
|
||||
|
||||
AUDIO_TRACK_FIELD_REGEX = re.compile(
|
||||
r'^\(([^)]*)\)\s*\(([^)]*?)\s*ch\)\s*' +
|
||||
r'((?:\([^()]*\)\s*)*)\(iso639-2:\s*([^)]+)\)$')
|
||||
|
||||
AudioTrack = namedtuple('AudioTrack',
|
||||
'number lang codec channels iso639_2 extras')
|
||||
|
||||
def ParseAudioTracks(d):
|
||||
for number, info in sorted(((int(n), info) for (n, info) in d.items())):
|
||||
m = AUDIO_TRACK_REGEX.match(info)
|
||||
if m:
|
||||
lang, field_string, extras = m.groups()
|
||||
m2 = AUDIO_TRACK_FIELD_REGEX.match(field_string)
|
||||
if m2:
|
||||
codec, channels, more_extras, iso639_2 = m2.groups()
|
||||
if more_extras:
|
||||
extras = more_extras + extras
|
||||
yield AudioTrack(number, lang, codec, channels,
|
||||
iso639_2, extras)
|
||||
else:
|
||||
warn('Cannot parse audio track fields %r' % field_string)
|
||||
else:
|
||||
warn('Cannot parse audio track info %r' % info)
|
||||
|
||||
SUB_TRACK_REGEX = re.compile(
|
||||
r'^(\S(?:.*\S)?)\s+\(iso639-2:\s*([^)]+)\)\s*((?:\S(?:.*\S)?)?)$')
|
||||
|
||||
SubtitleTrack = namedtuple('SubtitleTrack',
|
||||
'number name iso639_2 extras')
|
||||
|
||||
def ParseSubtitleTracks(d):
|
||||
for number, info in sorted(((int(n), info) for (n, info) in d.items())):
|
||||
m = SUB_TRACK_REGEX.match(info)
|
||||
assert m, 'UNMATCHED %r' % info
|
||||
name, iso639_2, extras = m.groups()
|
||||
yield SubtitleTrack(number, name, iso639_2, extras)
|
||||
|
||||
def RenderBar(start, length, total, width):
|
||||
end = start + length
|
||||
start = int(round(start * (width - 1) / total))
|
||||
length = int(round(end * (width - 1) / total)) - start + 1
|
||||
return ('‥' * start +
|
||||
'■' * length +
|
||||
'‥' * (width - start - length))
|
||||
|
||||
MAX_BAR_WIDTH = 50
|
||||
|
||||
def DisplayScan(titles):
|
||||
max_title_seconds = max(
|
||||
title.info['duration'].in_seconds()
|
||||
for title in titles)
|
||||
|
||||
for title in titles:
|
||||
info = title.info
|
||||
size = ParseSize(info['size'])
|
||||
xaspect, yaspect = ComputeAspectRatio(size)
|
||||
duration = info['duration']
|
||||
title_seconds = duration.in_seconds()
|
||||
print('Title % 3d/% 3d: %s %d×%d %d:%d %3g fps' %
|
||||
(title.number, len(titles), duration, size.width,
|
||||
size.height, xaspect, yaspect, size.fps))
|
||||
for at in ParseAudioTracks(info['audio tracks']):
|
||||
print(' audio % 3d: %s (%sch) [%s]' %
|
||||
(at.number, at.lang, at.channels, at.extras))
|
||||
for sub in ParseSubtitleTracks(info['subtitle tracks']):
|
||||
print(' sub % 3d: %s [%s]' %
|
||||
(sub.number, sub.name, sub.extras))
|
||||
position = 0
|
||||
if title_seconds > 0:
|
||||
for chapter in ParseChapters(info['chapters']):
|
||||
seconds = chapter.duration.in_seconds()
|
||||
bar_width = int(round(
|
||||
MAX_BAR_WIDTH * title_seconds / max_title_seconds))
|
||||
bar = RenderBar(position, seconds, title_seconds, bar_width)
|
||||
print(' chapter % 3d: %s ◖%s◗'
|
||||
% (chapter.number, chapter.duration, bar))
|
||||
position += seconds
|
||||
print()
|
||||
|
||||
def ParseArgs():
|
||||
description, epilog = __doc__.strip().split('\n', 1)
|
||||
parser = argparse.ArgumentParser(description=description, epilog=epilog,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument('-v', '--verbose',
|
||||
action='store_true',
|
||||
help="Increase verbosity.")
|
||||
parser.add_argument('-c', '--chapter_split',
|
||||
action='store_true',
|
||||
help="Split each chapter out into a separate file.")
|
||||
parser.add_argument('-n', '--dry-run',
|
||||
action='store_true',
|
||||
help="Don't actually write anything.")
|
||||
parser.add_argument('--scan',
|
||||
action='store_true',
|
||||
help="Display scan of disc; do not rip.")
|
||||
parser.add_argument('--main-feature',
|
||||
action='store_true',
|
||||
help="Rip only the main feature title.")
|
||||
parser.add_argument('-t', '--titles',
|
||||
default="*",
|
||||
help="""Comma-separated list of title numbers to consider
|
||||
(starting at 1) or * for all titles.""")
|
||||
parser.add_argument('-i', '--input',
|
||||
help="Volume to rip (must be a directory).")
|
||||
parser.add_argument('-o', '--output',
|
||||
help="""Output location. Extension is added if only one title
|
||||
being ripped, otherwise, a directory will be created to contain
|
||||
ripped titles.""")
|
||||
parser.add_argument('--mount-timeout',
|
||||
default=15,
|
||||
help="Amount of time to wait for a mountpoint to be mounted",
|
||||
type=float)
|
||||
args = parser.parse_args()
|
||||
if not args.scan and args.output is None:
|
||||
raise UserError("output argument is required")
|
||||
return args
|
||||
|
||||
# TODO: make it possible to have ranges with no end (meaning they end at last
|
||||
# title)
|
||||
NUM_RANGE_REGEX = re.compile(r'^(\d*)-(\d+)|(\d+)$')
|
||||
def parse_titles_arg(titles_arg):
|
||||
if titles_arg == '*':
|
||||
return None # all titles
|
||||
else:
|
||||
def str_to_ints(s):
|
||||
m = NUM_RANGE_REGEX.match(s)
|
||||
if not m :
|
||||
raise UserError(
|
||||
"--titles must be * or list of integer ranges, found %r" %
|
||||
titles_arg)
|
||||
else:
|
||||
start,end,only = m.groups()
|
||||
if only is not None:
|
||||
return [int(only)]
|
||||
else:
|
||||
start = int(start) if start else 1
|
||||
end = int(end)
|
||||
return range(start, end + 1)
|
||||
result = set()
|
||||
for s in titles_arg.split(','):
|
||||
result.update(str_to_ints(s))
|
||||
result = sorted(list(result))
|
||||
return result
|
||||
|
||||
def main():
|
||||
args = ParseArgs()
|
||||
dvd = DVD(args.input, args.verbose, args.mount_timeout)
|
||||
print('Reading from %r' % dvd.mountpoint)
|
||||
title_numbers = parse_titles_arg(args.titles)
|
||||
titles = tuple(dvd.ScanTitles(title_numbers, args.verbose))
|
||||
|
||||
if args.scan:
|
||||
DisplayScan(titles)
|
||||
else:
|
||||
if args.main_feature and len(titles) > 1:
|
||||
# TODO: make this affect scan as well
|
||||
titles = [FindMainFeature(titles, args.verbose)]
|
||||
|
||||
if not titles:
|
||||
raise UserError("No titles to rip")
|
||||
else:
|
||||
if not args.output:
|
||||
raise UserError("No output specified")
|
||||
print('Writing to %r' % args.output)
|
||||
tasks = tuple(ConstructTasks(titles, args.chapter_split))
|
||||
|
||||
filenames = TaskFilenames(tasks, args.output, dry_run=args.dry_run)
|
||||
# Don't stomp on existing files
|
||||
for filename in filenames:
|
||||
if os.path.exists(filename):
|
||||
raise UserError('%r already exists' % filename)
|
||||
|
||||
PerformTasks(dvd, tasks, len(titles), filenames,
|
||||
dry_run=args.dry_run, verbose=args.verbose)
|
||||
|
||||
print('=' * 78)
|
||||
if not args.dry_run:
|
||||
dvd.Eject()
|
||||
|
||||
def warn(msg):
|
||||
print('warning: %s' % (msg,), file=sys.stderr)
|
||||
|
||||
if __name__ == '__main__':
|
||||
error = None
|
||||
try:
|
||||
main()
|
||||
except FileExistsError as exc:
|
||||
error = '%s: %r' % (exc.strerror, exc.filename)
|
||||
except UserError as exc:
|
||||
error = exc.message
|
||||
|
||||
if error is not None:
|
||||
print('%s: error: %s'
|
||||
% (os.path.basename(sys.argv[0]), error), file=sys.stderr)
|
||||
sys.exit(1)
|
|
@ -119,5 +119,8 @@ alias lc="colorls -r"
|
|||
|
||||
alias open="$TERMINAL & disown"
|
||||
|
||||
#audiable to mp3
|
||||
alias aa2mp3='ffmpeg -f concat -safe 0 -i <(for f in *.aa; do echo "file '"'"'$(pwd)/$f'"'"'";done) output.mp3'
|
||||
|
||||
# Fix Typos
|
||||
alias cim="vim"
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 80042f404d193ed1919d845bee56faee78312190
|
||||
Subproject commit 20137dafd8536d5f5af872bbc51e72bed8fb81a2
|
1
pandoc/templates/template-letter.latex
Symbolic link
1
pandoc/templates/template-letter.latex
Symbolic link
|
@ -0,0 +1 @@
|
|||
template-letter.tex
|
|
@ -225,11 +225,11 @@ $if(return-address)$
|
|||
$else$
|
||||
\address{
|
||||
\textbf{Jonathan Hodgson} \\
|
||||
Cherrydown, The Meadows,\\
|
||||
Station Road,\\
|
||||
Cotton,\\
|
||||
11 Lees Court,\\
|
||||
Glemsford,\\
|
||||
Sudbury,\\
|
||||
Suffolk,\\
|
||||
IP14 4NZ \\[0.2cm]
|
||||
CO10 7SW \\[0.2cm]
|
||||
\textbf{Tel:} \href{tel:+447753492267}{07753 492267} \\
|
||||
\textbf{Email:} \email
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue