refactor: writer.py vira pacote, um módulo por assunto
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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1bebee4359
commit
4f5cf94443
@@ -0,0 +1,94 @@
|
||||
"""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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user