fix styled ASS

This commit is contained in:
Javanaut
2026-05-22 21:04:50 +02:00
parent 20ab08626b
commit 87568989fe
10 changed files with 262 additions and 10 deletions

View File

@@ -1139,6 +1139,7 @@ def convert(ctx,
from ffx.tmdb_controller import TmdbController
from ffx.track_codec import TrackCodec
from ffx.track_disposition import TrackDisposition
from ffx.track_type import TrackType
from ffx.video_encoder import VideoEncoder
startTime = time.perf_counter()
@@ -1436,7 +1437,8 @@ def convert(ctx,
else:
targetMediaDescriptor = currentPattern.getMediaDescriptor(ctx.obj)
if styledAssSourceDetected:
targetMediaDescriptor = targetMediaDescriptor.withoutAttachmentTracks(
targetMediaDescriptor = targetMediaDescriptor.withSourceAttachmentTracks(
sourceMediaDescriptor,
AttachmentFormat.TTF,
context=ctx.obj,
)
@@ -1449,6 +1451,8 @@ def convert(ctx,
targetTrackDescriptorList = targetMediaDescriptor.getTrackDescriptors()
for ttd in targetTrackDescriptorList:
if ttd.getType() == TrackType.ATTACHMENT:
continue
tti = ttd.getIndex()
ttsi = ttd.getSourceIndex()

View File

@@ -372,6 +372,53 @@ class MediaDescriptor:
filteredMediaDescriptor.reindexSubIndices()
return filteredMediaDescriptor
def withoutAttachmentsForComparison(self):
return self.withoutAttachmentTracks(context=self.__context)
def withSourceAttachmentTracks(
self,
sourceMediaDescriptor: Self,
attachmentFormat: AttachmentFormat | None = None,
context: dict | None = None,
):
trackDescriptors = []
for trackDescriptor in self.__trackDescriptors:
if trackDescriptor.getType() == TrackType.ATTACHMENT and (
attachmentFormat is None
or trackDescriptor.getAttachmentFormat() == attachmentFormat
):
continue
trackDescriptors.append(
trackDescriptor.clone(
context=context if context is not None else self.__context
)
)
for sourceTrackDescriptor in sourceMediaDescriptor.getAttachmentTracks():
if (
attachmentFormat is not None
and sourceTrackDescriptor.getAttachmentFormat() != attachmentFormat
):
continue
attachmentClone = sourceTrackDescriptor.clone(
context=context if context is not None else self.__context
)
attachmentClone.setIndex(len(trackDescriptors))
trackDescriptors.append(attachmentClone)
kwargs = {
MediaDescriptor.TAGS_KEY: dict(self.__mediaTags),
MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY: trackDescriptors,
}
if context is not None:
kwargs[MediaDescriptor.CONTEXT_KEY] = context
elif self.__context:
kwargs[MediaDescriptor.CONTEXT_KEY] = self.__context
mergedMediaDescriptor = MediaDescriptor(**kwargs)
mergedMediaDescriptor.reindexSubIndices()
return mergedMediaDescriptor
def getImportFileTokens(self, use_sub_index: bool = True):
"""Generate ffmpeg import options for external stream files"""

View File

@@ -56,8 +56,24 @@ class MediaDescriptorChangeSet():
and 'ignore' in metadataConfiguration['streams'].keys() else [])
self.__targetTrackDescriptors = targetMediaDescriptor.getTrackDescriptors() if targetMediaDescriptor is not None else []
self.__sourceTrackDescriptors = sourceMediaDescriptor.getTrackDescriptors() if sourceMediaDescriptor is not None else []
self.__targetTrackDescriptors = (
[
trackDescriptor
for trackDescriptor in targetMediaDescriptor.getTrackDescriptors()
if trackDescriptor.getType() != TrackType.ATTACHMENT
]
if targetMediaDescriptor is not None
else []
)
self.__sourceTrackDescriptors = (
[
trackDescriptor
for trackDescriptor in sourceMediaDescriptor.getTrackDescriptors()
if trackDescriptor.getType() != TrackType.ATTACHMENT
]
if sourceMediaDescriptor is not None
else []
)
self.__targetTrackDescriptorsByIndex = {
trackDescriptor.getIndex(): trackDescriptor
for trackDescriptor in self.__targetTrackDescriptors

View File

@@ -166,10 +166,9 @@ class MediaWorkflowScreenBase(Screen):
self._baselineMediaDescriptor = probedMediaDescriptor
self._sourceMediaDescriptor = probedMediaDescriptor
self._currentPattern = self._mediaFileProperties.getPattern()
self._targetMediaDescriptor = (
self._currentPattern.getMediaDescriptor(self.context)
if self._currentPattern is not None
else None
self._targetMediaDescriptor = self._resolve_target_media_descriptor(
self._currentPattern,
self._sourceMediaDescriptor,
)
self.rebuildChangeSet()
@@ -205,6 +204,20 @@ class MediaWorkflowScreenBase(Screen):
def getTrackEditSourceDescriptor(self) -> TrackDescriptor | None:
return self.getSelectedTrackDescriptor()
def _resolve_target_media_descriptor(self, currentPattern, sourceMediaDescriptor):
if currentPattern is None:
return None
targetMediaDescriptor = currentPattern.getMediaDescriptor(self.context)
if sourceMediaDescriptor.hasStyledAssSubtitlesWithFontAttachments():
targetMediaDescriptor = targetMediaDescriptor.withSourceAttachmentTracks(
sourceMediaDescriptor,
AttachmentFormat.TTF,
context=self.context,
)
return targetMediaDescriptor
def updateMediaTags(self):
displayedMediaDescriptor = self.getDisplayedMediaDescriptor()
self._sourceMediaTagRowData = populate_tag_table(

View File

@@ -7,6 +7,7 @@ from .show import Base, Show
from ffx.media_descriptor import MediaDescriptor
from ffx.show_descriptor import ShowDescriptor
from ffx.track_type import TrackType
class Pattern(Base):
@@ -76,6 +77,8 @@ class Pattern(Base):
subIndexCounter = {}
for track in self.tracks:
trackType = track.getType()
if trackType == TrackType.ATTACHMENT:
continue
if not trackType in subIndexCounter.keys():
subIndexCounter[trackType] = 0
kwargs[MediaDescriptor.TRACK_DESCRIPTOR_LIST_KEY].append(track.getDescriptor(context, subIndex = subIndexCounter[trackType]))

View File

@@ -8,6 +8,7 @@ from ffx.model.track import Track
from ffx.model.track_tag import TrackTag
from ffx.track_descriptor import TrackDescriptor
from ffx.track_disposition import TrackDisposition
from ffx.track_type import TrackType
class DuplicatePatternMatchError(click.ClickException):
@@ -86,12 +87,16 @@ class PatternController:
)
normalized_descriptors = []
filtered_attachments = False
for trackDescriptor in trackDescriptors:
if type(trackDescriptor) is not TrackDescriptor:
raise TypeError(
"PatternController: All track descriptors are required to be of type TrackDescriptor"
)
normalized_descriptors.append(trackDescriptor)
if trackDescriptor.getType() == TrackType.ATTACHMENT:
filtered_attachments = True
continue
normalized_descriptors.append(trackDescriptor.clone())
if not normalized_descriptors:
raise InvalidPatternSchemaError(
@@ -102,6 +107,10 @@ class PatternController:
normalized_descriptors, key=lambda descriptor: descriptor.getIndex()
)
if filtered_attachments:
for index, descriptor in enumerate(normalized_descriptors):
descriptor.setIndex(index)
index_set = {descriptor.getIndex() for descriptor in normalized_descriptors}
expected_indexes = set(range(len(normalized_descriptors)))
if index_set != expected_indexes:
@@ -170,7 +179,7 @@ class PatternController:
pattern.tracks.append(self._build_track_row(trackDescriptor))
def _validate_persisted_pattern(self, pattern: Pattern):
if not pattern.tracks:
if not any(track.getType() != TrackType.ATTACHMENT for track in pattern.tracks):
raise InvalidPatternSchemaError(
f"Pattern #{pattern.getId()} ({pattern.getPattern()!r}) is invalid because it has no tracks."
)

View File

@@ -35,6 +35,8 @@ class TrackController():
def addTrack(self, trackDescriptor : TrackDescriptor, patternId = None):
if trackDescriptor.getType() == TrackType.ATTACHMENT:
return False
# option to override pattern id in case track descriptor has not set it
patId = int(trackDescriptor.getPatternId() if patternId is None else patternId)
@@ -72,6 +74,8 @@ class TrackController():
if type(trackDescriptor) is not TrackDescriptor:
raise TypeError('TrackController.updateTrack(): Argument trackDescriptor is required to be of type TrackDescriptor')
if trackDescriptor.getType() == TrackType.ATTACHMENT:
return False
try:
s = self.Session()

View File

@@ -13,6 +13,7 @@ if str(SRC_ROOT) not in sys.path:
from ffx.media_descriptor import MediaDescriptor # noqa: E402
from ffx.media_descriptor_change_set import MediaDescriptorChangeSet # noqa: E402
from ffx.attachment_format import AttachmentFormat # noqa: E402
from ffx.track_descriptor import TrackDescriptor # noqa: E402
from ffx.track_type import TrackType # noqa: E402
from ffx.i18n import set_current_language # noqa: E402
@@ -436,6 +437,47 @@ class MediaDescriptorChangeSetTests(unittest.TestCase):
self.assertNotIn("creation_time=", metadata_tokens)
self.assertNotIn("BPS=", metadata_tokens)
def test_attachment_tracks_are_ignored_for_pattern_comparison(self):
context = {
"logger": get_ffx_logger(),
"config": StaticConfig({}),
}
source_track = TrackDescriptor(
index=0,
source_index=0,
sub_index=0,
track_type=TrackType.ATTACHMENT,
attachment_format=AttachmentFormat.TTF,
tags={"filename": "current.ttf", "mimetype": "font/ttf"},
)
target_track = TrackDescriptor(
index=0,
source_index=0,
sub_index=0,
track_type=TrackType.ATTACHMENT,
attachment_format=AttachmentFormat.TTF,
tags={"filename": "stored.ttf", "mimetype": "font/ttf"},
)
stale_target_track = TrackDescriptor(
index=1,
source_index=1,
sub_index=1,
track_type=TrackType.ATTACHMENT,
attachment_format=AttachmentFormat.TTF,
tags={"filename": "missing.ttf", "mimetype": "font/ttf"},
)
change_set = MediaDescriptorChangeSet(
context,
MediaDescriptor(track_descriptors=[target_track, stale_target_track]),
MediaDescriptor(track_descriptors=[source_track]),
)
self.assertEqual({}, change_set.getChangeSetObj())
self.assertEqual([], change_set.generateMetadataTokens())
self.assertEqual([], change_set.generateDispositionTokens())
def test_normalization_can_be_disabled_per_context(self):
context = {
"logger": get_ffx_logger(),

View File

@@ -193,6 +193,36 @@ class PatternManagementTests(unittest.TestCase):
self.assertIn("at least one track", str(caught.exception))
def test_save_pattern_schema_does_not_persist_attachment_tracks(self):
pattern_id = self.save_pattern(
1,
r"^noattachments_(s[0-9]+e[0-9]+)\.mkv$",
tracks=[
make_track_descriptor(0, track_type=TrackType.VIDEO),
make_track_descriptor(1, track_type=TrackType.ATTACHMENT),
],
)
Session = self.context["database"]["session"]
session = Session()
try:
tracks = session.query(Pattern).filter(Pattern.id == pattern_id).first().tracks
self.assertEqual(1, len(tracks))
self.assertEqual(TrackType.VIDEO, tracks[0].getType())
finally:
session.close()
def test_track_controller_does_not_add_attachment_tracks_to_patterns(self):
pattern_id = self.save_pattern(1, r"^skipadd_(s[0-9]+e[0-9]+)\.mkv$")
added = self.track_controller.addTrack(
make_track_descriptor(1, track_type=TrackType.ATTACHMENT),
patternId=pattern_id,
)
self.assertFalse(added)
self.assertEqual(1, len(self.track_controller.findTracks(pattern_id)))
def test_match_filename_rejects_existing_trackless_pattern_rows(self):
self.insert_trackless_pattern_row(1, r"^invalid_(s[0-9]+e[0-9]+)\.mkv$")

View File

@@ -15,12 +15,13 @@ if str(SRC_ROOT) not in sys.path:
from ffx.audio_layout import AudioLayout # noqa: E402
from ffx.attachment_format import AttachmentFormat # noqa: E402
from ffx.helper import DIFF_ADDED_KEY # noqa: E402
from ffx.helper import DIFF_ADDED_KEY, DIFF_REMOVED_KEY # noqa: E402
from ffx.iso_language import IsoLanguage # noqa: E402
from ffx.logging_utils import get_ffx_logger # noqa: E402
from ffx.inspect_details_screen import InspectDetailsScreen # noqa: E402
from ffx.i18n import set_current_language # noqa: E402
from ffx.media_descriptor import MediaDescriptor # noqa: E402
from ffx.media_descriptor_change_set import MediaDescriptorChangeSet # noqa: E402
from ffx.media_edit_screen import MediaEditScreen # noqa: E402
from ffx.pattern_details_screen import PatternDetailsScreen # noqa: E402
from ffx.show_descriptor import ShowDescriptor # noqa: E402
@@ -822,6 +823,89 @@ class TagTableScreenStateTests(unittest.TestCase):
self.assertEqual("unknown", row[3])
self.assertEqual(" ", row[5])
def test_inspect_details_screen_uses_source_font_attachments_for_styled_ass(self):
class _Config:
def getData(self):
return {}
class _Pattern:
def __init__(self, media_descriptor):
self._media_descriptor = media_descriptor
def getMediaDescriptor(self, _context):
return self._media_descriptor
source_descriptor = MediaDescriptor(
track_descriptors=[
TrackDescriptor(
index=0,
source_index=0,
sub_index=0,
track_type=TrackType.SUBTITLE,
codec_name=TrackCodec.ASS,
tags={"title": "Styled Subtitle"},
),
TrackDescriptor(
index=1,
source_index=1,
sub_index=0,
track_type=TrackType.ATTACHMENT,
attachment_format=AttachmentFormat.TTF,
tags={"filename": "current.ttf", "mimetype": "font/ttf"},
),
]
)
pattern_descriptor = MediaDescriptor(
track_descriptors=[
TrackDescriptor(
index=0,
source_index=0,
sub_index=0,
track_type=TrackType.SUBTITLE,
codec_name=TrackCodec.ASS,
tags={"title": "Styled Subtitle"},
),
TrackDescriptor(
index=1,
source_index=1,
sub_index=0,
track_type=TrackType.ATTACHMENT,
attachment_format=AttachmentFormat.TTF,
tags={"filename": "old.ttf", "mimetype": "font/ttf"},
),
TrackDescriptor(
index=2,
source_index=2,
sub_index=1,
track_type=TrackType.ATTACHMENT,
attachment_format=AttachmentFormat.TTF,
tags={"filename": "missing.ttf", "mimetype": "font/ttf"},
),
]
)
screen = object.__new__(InspectDetailsScreen)
screen.context = {"logger": get_ffx_logger(), "config": _Config()}
resolved_descriptor = screen._resolve_target_media_descriptor(
_Pattern(pattern_descriptor),
source_descriptor,
)
attachment_tracks = resolved_descriptor.getAttachmentTracks()
self.assertEqual(1, len(attachment_tracks))
self.assertEqual({"filename": "current.ttf", "mimetype": "font/ttf"}, attachment_tracks[0].getTags())
change_set = MediaDescriptorChangeSet(
screen.context,
resolved_descriptor,
source_descriptor,
).getChangeSetObj()
self.assertNotIn(
1,
change_set.get("tracks", {}).get(DIFF_REMOVED_KEY, {}),
)
def test_inspect_details_screen_maps_target_selection_back_to_source_track(self):
source_track = TrackDescriptor(
index=3,