import os import re import bpy from ... import config from ...core import Check, Issue, Severity, contexts def _sound_strips(scene): editor = scene.sequence_editor if editor is None: return [] return [strip for strip in editor.strips_all if strip.type == "SOUND"] def latest_version(path): """Return (current, latest, latest_path) for a versioned path, or None. The version token (config VERSION_PATTERN) can be in the file name (``dialog_v003.wav``) and/or in a folder (``.../v003/dialog.wav``). Every token is tried; when a folder is bumped, the same token is updated in the rest of the path. """ version_re = re.compile(config.VERSION_PATTERN) parts = os.path.normpath(path).split(os.sep) current = None best, best_path = None, path for index, part in enumerate(parts): match = version_re.search(part) if not match: continue current_text = match.group(1) if current is None: current = best = int(current_text) parent = os.sep.join(parts[:index]) or os.sep prefix, suffix = part[: match.start(1)], part[match.end(1):] sibling_re = re.compile(re.escape(prefix) + r"(\d+)" + re.escape(suffix) + "$") try: siblings = os.listdir(parent) except OSError: continue for name in siblings: sibling = sibling_re.match(name) if not sibling or int(sibling.group(1)) <= best: continue new_text = sibling.group(1) rest = [p.replace(f"v{current_text}", f"v{new_text}") for p in parts[index + 1:]] candidate = os.sep.join([parent, name, *rest]) if os.path.exists(candidate): best, best_path = int(new_text), candidate if current is None: return None return current, best, best_path class SoundVersionCheck(Check): id = "sound_versions" label = "Sound versions" description = "Sound strips exist on disk and use the latest published version." category = "Sound" contexts = frozenset({contexts.LAYOUT}) order = 10 fix_description = "Point the sounds to their latest version" def run(self, ctx): for strip in _sound_strips(ctx.scene): sound = strip.sound if sound is None: yield Issue(f"Strip '{strip.name}' has no sound") continue if sound.packed_file is not None: yield Issue.for_id(f"'{strip.name}': sound is packed, cannot check its version", sound) continue path = bpy.path.abspath(sound.filepath, library=sound.library) if not os.path.isfile(path): yield Issue.for_id(f"'{strip.name}': file not found {path}", sound) continue versions = latest_version(path) if versions is None: yield Issue.for_id( f"'{strip.name}': no version number in {os.path.basename(path)}", sound, severity=Severity.WARNING, ) continue current, latest, latest_path = versions if latest > current: yield Issue.for_id( f"'{strip.name}': v{current:03d} used, v{latest:03d} available", sound, data={"path": latest_path}, ) def fix(self, ctx, issues): for issue in issues: sound = issue.get_id() path = issue.data.get("path") if sound is None or not path: continue relative = sound.filepath.startswith("//") # Setting the path reloads the sound. sound.filepath = bpy.path.relpath(path) if relative and bpy.data.filepath else path class SoundTrackActiveCheck(Check): id = "sound_active" label = "Sound track active" description = "The scene has sound, audio is not muted, and no sound strip or channel is muted." category = "Sound" contexts = frozenset({contexts.LAYOUT}) order = 20 fix_description = "Unmute the scene audio, sound strips and their channels" def run(self, ctx): scene = ctx.scene strips = _sound_strips(scene) if not strips: yield Issue("No sound strip in the sequencer") return if scene.use_audio: yield Issue("Scene audio is muted (Scene > Audio > Mute)", data={"scene": True}) channels = scene.sequence_editor.channels for strip in strips: if strip.mute: yield Issue(f"Sound strip '{strip.name}' is muted", data={"strip": strip.name}) if channels[strip.channel].mute: yield Issue( f"Channel {strip.channel} of '{strip.name}' is muted", data={"channel": strip.channel} ) def fix(self, ctx, issues): scene = ctx.scene editor = scene.sequence_editor for issue in issues: if issue.data.get("scene"): scene.use_audio = False if "strip" in issue.data: strip = editor.strips_all.get(issue.data["strip"]) if strip is not None: strip.mute = False if "channel" in issue.data: editor.channels[issue.data["channel"]].mute = False