O writer tinha 4.199 linhas, das quais 3.300 numa única classe com dezoito
assuntos dentro. Achar o trecho de zoom exigia rolar por marcadores,
velocidade e legendas.
Agora é o pacote fcpxml/writer/, com um arquivo por assunto e o
FCPXMLModifier montado por composição de mixins. Mixins, e não objetos
separados, porque todas essas operações mexem no mesmo documento e nos
mesmos índices — separá-las em objetos independentes transformaria toda
chamada interna em travessia de fronteira sem nada em troca. A divisão que
importa aqui é de leitura, não de estado.
Nenhuma mudança de comportamento e nenhuma alteração nos ~50 pontos que
importam do writer: o __init__ re-exporta tudo, inclusive os nomes com
underscore que a suíte já usava.
core 723 carga, índices, navegação na spine, save
titles 600 títulos e legendas dinâmicas
cut 333 dividir, cortar faixas, apagar
speed 297 velocidade e zoom
(+ 20 módulos menores)
Único ajuste de chamada: quatro testes faziam patch em
fcpxml.writer.subprocess, que agora mora em writer.document (ver
Engine/docs/05_EXPERIENCIAS.md #23).
Lint zerado, 1441 testes passando.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
95 lines
3.5 KiB
Python
95 lines
3.5 KiB
Python
"""Repontar a mídia de um projeto para novos arquivos.
|
|
|
|
Extraído de writer.py — ver fcpxml/writer/__init__.py para o conjunto.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
from typing import Any, Dict
|
|
|
|
|
|
class RelinkMixin:
|
|
"""Repontar a mídia de um projeto para novos arquivos."""
|
|
|
|
# ========================================================================
|
|
# MEDIA RELINK
|
|
# ========================================================================
|
|
|
|
def relink_media(
|
|
self,
|
|
find: str,
|
|
replace: str,
|
|
dry_run: bool = False,
|
|
) -> Dict[str, Any]:
|
|
"""Bulk-rewrite media source paths (programmatic relink).
|
|
|
|
Rewrites the ``src`` of every ``<asset>`` / ``<media-rep>`` whose
|
|
path starts with *find*, substituting *replace* — the standard
|
|
technique for relinking a moved or renamed media folder without
|
|
opening Final Cut Pro. FCP relinks via the ``media-rep`` file URL
|
|
on import; the device-specific bookmark blob is left untouched
|
|
(FCP regenerates it).
|
|
|
|
*find* / *replace* accept plain paths (``/Volumes/OldDrive``) or
|
|
``file://`` URLs; percent-encoding in existing URLs is handled.
|
|
Matching is prefix-based on whole path segments, so ``/Media/A``
|
|
matches ``/Media/A/clip.mov`` but not ``/Media/AB/clip.mov``.
|
|
|
|
Args:
|
|
find: Old path prefix to match.
|
|
replace: New path prefix to substitute.
|
|
dry_run: When True, report what would change without
|
|
mutating the tree.
|
|
|
|
Returns:
|
|
Summary dict: ``total_assets``, ``relinked`` (reference
|
|
count), ``dry_run``, and ``changes`` — a list of
|
|
``{asset, old, new, target_exists}`` entries
|
|
(``target_exists`` checks the new path on this machine).
|
|
"""
|
|
from urllib.parse import quote, unquote, urlparse
|
|
|
|
def _to_path(value: str) -> str:
|
|
if value.startswith('file://'):
|
|
return unquote(urlparse(value).path)
|
|
return value
|
|
|
|
find_path = _to_path(find).rstrip('/')
|
|
replace_path = _to_path(replace).rstrip('/')
|
|
if not find_path:
|
|
raise ValueError("relink_media: 'find' must be a non-empty path prefix")
|
|
|
|
changes = []
|
|
for asset_id, info in self.resources.items():
|
|
elem = info['element']
|
|
targets = [(elem, elem.get('src'))]
|
|
media_rep = elem.find('media-rep')
|
|
if media_rep is not None:
|
|
targets.append((media_rep, media_rep.get('src')))
|
|
|
|
for node, old_src in targets:
|
|
if not old_src:
|
|
continue
|
|
was_url = old_src.startswith('file://')
|
|
old_path = _to_path(old_src)
|
|
if old_path != find_path and not old_path.startswith(find_path + '/'):
|
|
continue
|
|
new_path = replace_path + old_path[len(find_path):]
|
|
new_src = 'file://' + quote(new_path) if was_url else new_path
|
|
if not dry_run:
|
|
node.set('src', new_src)
|
|
info['src'] = new_src
|
|
changes.append({
|
|
'asset': info.get('name') or asset_id,
|
|
'old': old_src,
|
|
'new': new_src,
|
|
'target_exists': Path(new_path).exists(),
|
|
})
|
|
|
|
return {
|
|
'total_assets': len(self.resources),
|
|
'relinked': len(changes),
|
|
'dry_run': dry_run,
|
|
'changes': changes,
|
|
}
|
|
|