This is a followup from my first post on using DVRescue tools for capturing footage from my old camcorder.
So far I have captured footage from 6 tapes, which is a big win from my POV. This is footage I was worried had been lost forever, and every recovered frame is a bonus, including two friends' weddings from 20+ years ago.
That said, there were a few challenges getting this far:
- The deck transport controls are not reliable. Sending fast-forward / rewind commands only
works about 40% of the time. Also the deck
status
request is only accurate around 10% of the time, ending my hopes for completely unattended capturing. - Partial captures leave a
.dvrescue.xml
which is not overwritten on subsequent captures. So if you capture the full file later, then rundvpackager
, it uses thedvrescue.xml
from the first capture. - Automatic rewinding of missed frames does not seem to work well with out-of-order footage. For example, one tape I recorded an hour, then rewound and overwrote the first 20 minutes. So the recording timestamp jumps back an hour at the transition from the end of the last recording to the footage recorded an hour previously. DVRescue does not like that. Turning off the automatic rewind gave me a full capture.
While working out where to store this newly captured footage, I happened to find some of the files from when I first transferred some of the tapes back in 2009 (directly into a firewire-equipped macbook pro at the time).
I feel a bit better about needing a bespoke per-tape process now I know I don’t have to transfer all 50 tapes.
Me being me, I wrapped up the tooling and sanity checking into a CLI anyway, as I think it may be a useful example of some of my favourite coding techniques.
Having used github exclusively for the last few years, I thought it would be interesting to take a look at hosting this elsewhere. Codeberg looked like a reasonable bet, so I have started geeklib on codeberg
The main chunk is the batch
command (@app
is a Typer instance):
@app.command()
def batch(
project: Annotated[str, typer.Option()],
title: Annotated[str, typer.Option()] = "",
work: Path = WORK_DIR,
dest: Path = DEST_DIR,
clean: bool = False,
rewind_count: Annotated[int, typer.Option("--rewind")] = 0,
) -> None:
"""Automates the whole process."""
dva = DVArchive(project=project, work_dir=work, dest_dir=dest, title=title)
repack()
rewind(wait=True)
console.print(f"Ripping DV to {dva.capture_file}")
dva.rip(rewind=rewind_count)
rewind()
dva.check_capture()
dva.split_clips()
dva.check_split()
for clip in track(
list(dva.mkv_clips()), description="Encoding...", console=console
):
mp4_clip = dva.encode_hevc(clip)
dest = dva.upload(mp4_clip, apply=True)
console.log(f"{dest} copied")
if clean:
for _ in dva.clean(apply=True):
pass
With a few helper dataclasses:
@dataclass
class DVArchive:
"""Wrapper for a DV archiving operation.
Every DV Archive has a project- the overall reason for recording the tape.
Each DVArchive also has an optional title, to allow for multiple tapes within
the same project.
"""
project: str
work_dir: Path = WORK_DIR
dest_dir: Path = DEST_DIR
title: str = ""
def __post_init__(self) -> None:
"""Checks field values."""
if not re.match(r"\w+$", self.project):
raise ValueError(f"Project '{self.project}' contains non-word characters.")
if self.title and not re.match(r"\w+$", self.title):
raise ValueError(f"Title '{self.title}' contains non-word characters.")
if not self.work_dir.exists():
raise ValueError(f"Working directory {self.work_dir} does not exist.")
if not self.dest_dir.exists():
raise ValueError(f"Destination directory {self.dest_dir} does not exist.")
@property
def name(self) -> str:
"""Returns the canonical name of the project/title."""
if self.title:
return f"{self.project}-{self.title}"
return f"{self.project}"
@property
def capture_file(self) -> str:
"""Returns the name of the file containing captured DV footage."""
return f"{self.name}.dv"
def rip(self, timeout: int = 30, rewind: int = 3) -> None:
"""Transfers footage from the camera to a local file."""
if (self.work_dir / self.capture_file).exists():
raise
args = ["dvrescue", f"device://0x{DEVICE_ID}"]
if timeout:
args.extend(["--timeout", str(timeout)])
if rewind:
args.extend(["-y", "--rewind-count", "3"])
args.extend(["-m", self.capture_file])
_ = subprocess.run(args, check=True, cwd=self.work_dir)
...