fix styled ASS and refactor att format
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -10,6 +10,8 @@ tools/ansible/inventory/group_vars/all.yml
|
|||||||
ffx_test_report.log
|
ffx_test_report.log
|
||||||
bin/conversiontest.py
|
bin/conversiontest.py
|
||||||
|
|
||||||
|
tests/assets/
|
||||||
|
|
||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
|
|||||||
@@ -75,7 +75,9 @@
|
|||||||
|
|
||||||
Detect ffmpeg warning "Timestamps are unset in a packet for stream 0. This is deprecated and will stop working in the future. Fix your code to set the timestamps properly" and try autofix by -fflags +genpts -> Warning if fails -> Error. Check if flags collide with anything.
|
Detect ffmpeg warning "Timestamps are unset in a packet for stream 0. This is deprecated and will stop working in the future. Fix your code to set the timestamps properly" and try autofix by -fflags +genpts -> Warning if fails -> Error. Check if flags collide with anything.
|
||||||
|
|
||||||
|
<!--
|
||||||
|
|
||||||
## .265 export
|
## Source Formats
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
-map 0:v -c:v copy -bsf:v hevc_mp4toannexb out.h265
|
|
||||||
|
|||||||
170
docs/file_formats.md
Normal file
170
docs/file_formats.md
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
# File Formats
|
||||||
|
|
||||||
|
This document captures source-file-format notes that complement the normative
|
||||||
|
requirements in `requirements/source_file_formats.md`.
|
||||||
|
|
||||||
|
The first documented format is a Matroska source that carries styled ASS/SSA
|
||||||
|
subtitle streams together with embedded font attachments.
|
||||||
|
|
||||||
|
## Styled ASS In Matroska With Embedded Fonts
|
||||||
|
|
||||||
|
These files are typically `.mkv` releases where subtitle rendering quality
|
||||||
|
depends on keeping both parts of the subtitle package together:
|
||||||
|
|
||||||
|
- one or more subtitle streams with codec `ass`
|
||||||
|
- one or more attachment streams that embed font files used by those subtitles
|
||||||
|
|
||||||
|
This matters because ASS subtitles are not plain text subtitles in the narrow
|
||||||
|
WebVTT sense. They can carry layout, styling, positioning, karaoke, signs, and
|
||||||
|
other typesetting effects. If the matching embedded fonts are lost, consumers
|
||||||
|
can still see subtitle text but the intended styling and sometimes glyph
|
||||||
|
coverage can be degraded.
|
||||||
|
|
||||||
|
For FFX this format is special because the ASS subtitle streams should remain
|
||||||
|
normally editable and mappable, while the related font attachments should be
|
||||||
|
transported unchanged.
|
||||||
|
|
||||||
|
## Observed Sample
|
||||||
|
|
||||||
|
Assessment date: `2026-04-17`
|
||||||
|
|
||||||
|
Observed sample file:
|
||||||
|
|
||||||
|
- `tests/assets/boruto_s01e283_ssa.mkv`
|
||||||
|
|
||||||
|
Commands used for assessment:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ffprobe tests/assets/boruto_s01e283_ssa.mkv
|
||||||
|
ffprobe -hide_banner -show_format -show_streams -of json tests/assets/boruto_s01e283_ssa.mkv
|
||||||
|
```
|
||||||
|
|
||||||
|
Observed stream layout:
|
||||||
|
|
||||||
|
| Stream index | Kind | Key details |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `0` | video | `codec_name=h264` |
|
||||||
|
| `1` | audio | `codec_name=aac`, `language=jpn` |
|
||||||
|
| `2` | subtitle | `codec_name=ass`, `language=ger`, default |
|
||||||
|
| `3` | subtitle | `codec_name=ass`, `language=eng` |
|
||||||
|
| `4`-`13` | attachment | `tags.mimetype=font/ttf`, `.ttf` filenames |
|
||||||
|
|
||||||
|
Observed attachment filenames:
|
||||||
|
|
||||||
|
- `AmazonEmberTanuki-Italic.ttf`
|
||||||
|
- `AmazonEmberTanuki-Regular.ttf`
|
||||||
|
- `Arial.ttf`
|
||||||
|
- `Arial Bold.ttf`
|
||||||
|
- `Georgia.ttf`
|
||||||
|
- `Times New Roman.ttf`
|
||||||
|
- `Times New Roman Bold.ttf`
|
||||||
|
- `Trebuchet MS.ttf`
|
||||||
|
- `Verdana.ttf`
|
||||||
|
- `Verdana Bold.ttf`
|
||||||
|
|
||||||
|
Important probe behavior from the real sample:
|
||||||
|
|
||||||
|
- Plain `ffprobe` lists the font streams as `Attachment: none`.
|
||||||
|
- Plain `ffprobe` also prints warnings such as `Could not find codec
|
||||||
|
parameters for stream 4 (Attachment: none): unknown codec` and later
|
||||||
|
`Unsupported codec with id 0 for input stream ...`.
|
||||||
|
- The JSON produced by `FileProperties.FFPROBE_COMMAND_TOKENS`
|
||||||
|
(`ffprobe -hide_banner -show_format -show_streams -of json`) still exposes
|
||||||
|
the attachment streams clearly through `codec_type="attachment"` and the
|
||||||
|
attachment tags.
|
||||||
|
- In that JSON, the attachment streams do not expose `codec_name`.
|
||||||
|
|
||||||
|
This last point is important for FFX: robust detection must not depend on
|
||||||
|
attachment `codec_name` being present.
|
||||||
|
|
||||||
|
## Detection Guidance
|
||||||
|
|
||||||
|
Current known indicators for this format are:
|
||||||
|
|
||||||
|
- one or more subtitle streams with `codec_type="subtitle"` and
|
||||||
|
`codec_name="ass"`
|
||||||
|
- one or more attachment streams with `codec_type="attachment"`
|
||||||
|
- attachment tags that identify embedded fonts, especially
|
||||||
|
`tags.mimetype="font/ttf"`
|
||||||
|
- attachment filenames that end in `.ttf`
|
||||||
|
|
||||||
|
The pattern can vary. FFX should therefore treat the above as a cluster of
|
||||||
|
signals rather than an exact signature tied to one file.
|
||||||
|
|
||||||
|
Inference from the observed sample plus FFmpeg documentation:
|
||||||
|
|
||||||
|
- MIME matching should not be limited to `font/ttf` alone.
|
||||||
|
- The Boruto sample uses `font/ttf`.
|
||||||
|
- FFmpeg's Matroska attachment example uses
|
||||||
|
`mimetype=application/x-truetype-font` for a `.ttf` attachment.
|
||||||
|
- Detection should therefore normalize multiple TTF-like MIME values rather
|
||||||
|
than depend on a single exact string.
|
||||||
|
|
||||||
|
## Processing Expectations In FFX
|
||||||
|
|
||||||
|
The format-specific requirements live in
|
||||||
|
`requirements/source_file_formats.md`. In practical terms, FFX should:
|
||||||
|
|
||||||
|
- recognize the ASS-plus-font-attachment pattern even when attachment probe
|
||||||
|
data is incomplete
|
||||||
|
- tell the operator that the pattern was detected and that special handling is
|
||||||
|
being used
|
||||||
|
- reject sidecar subtitle import for such sources, because converting or
|
||||||
|
replacing these subtitle tracks with ordinary external text subtitles would
|
||||||
|
break the intended subtitle package
|
||||||
|
- continue to allow normal manipulation of the ASS subtitle tracks themselves
|
||||||
|
- preserve the font attachment streams unchanged
|
||||||
|
|
||||||
|
## FFmpeg Notes
|
||||||
|
|
||||||
|
Relevant FFmpeg documentation confirms several behaviors that line up with
|
||||||
|
FFX's needs:
|
||||||
|
|
||||||
|
- FFmpeg documents `-attach` as adding an attachment stream to the output, and
|
||||||
|
explicitly names Matroska fonts used in subtitle rendering as an example.
|
||||||
|
- FFmpeg documents attachment streams as regular streams that are created after
|
||||||
|
the mapped media streams.
|
||||||
|
- FFmpeg documents `-dump_attachment` for extracting attachment streams, which
|
||||||
|
is useful for debugging or validating a source file's embedded fonts.
|
||||||
|
- FFmpeg's Matroska example requires a `mimetype` metadata tag for attached
|
||||||
|
fonts, which is consistent with using attachment tags as detection signals.
|
||||||
|
- FFmpeg also notes that attachments are implemented as codec extradata. That
|
||||||
|
helps explain why probe output for attachment streams can look different from
|
||||||
|
ordinary audio, video, and subtitle streams.
|
||||||
|
|
||||||
|
Implication for FFX:
|
||||||
|
|
||||||
|
- Attachment preservation is not an optional cosmetic feature for this format.
|
||||||
|
It is part of preserving the subtitle package correctly.
|
||||||
|
|
||||||
|
## Jellyfin Notes
|
||||||
|
|
||||||
|
Jellyfin's documentation also supports keeping this format intact:
|
||||||
|
|
||||||
|
- Jellyfin's subtitle compatibility table lists `ASS/SSA` as supported in
|
||||||
|
`MKV` and not supported in `MP4`.
|
||||||
|
- Jellyfin notes that when subtitles must be transcoded, they are either
|
||||||
|
converted to a supported format or burned into the video, and burning them in
|
||||||
|
is the most CPU-intensive path.
|
||||||
|
- Jellyfin's subtitle-extraction example for `SSA/ASS` first dumps attachment
|
||||||
|
streams and then extracts the ASS subtitle stream, which reflects the real
|
||||||
|
relationship between ASS subtitles and embedded fonts in MKV releases.
|
||||||
|
- Jellyfin's font documentation says text-based subtitles require fonts to
|
||||||
|
render properly.
|
||||||
|
- Jellyfin's configuration documentation says the web client uses configured
|
||||||
|
fallback fonts for ASS subtitles when other fonts such as MKV attachments or
|
||||||
|
client-side fonts are not available.
|
||||||
|
|
||||||
|
Inference from the Jellyfin compatibility tables:
|
||||||
|
|
||||||
|
- Keeping this subtitle format in Matroska is the safest interoperability
|
||||||
|
choice for Jellyfin consumers.
|
||||||
|
- Converting the subtitle payload to WebVTT would lose styled ASS behavior.
|
||||||
|
- Dropping the attachment streams would force client or fallback font
|
||||||
|
substitution and can change appearance or glyph coverage.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- FFmpeg documentation: https://ffmpeg.org/ffmpeg.html
|
||||||
|
- Jellyfin codec support: https://jellyfin.org/docs/general/clients/codec-support/
|
||||||
|
- Jellyfin configuration and fonts: https://jellyfin.org/docs/general/administration/configuration/
|
||||||
90
requirements/source_file_formats.md
Normal file
90
requirements/source_file_formats.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# Source File Formats
|
||||||
|
|
||||||
|
This file defines source-file-format-specific processing requirements for FFX.
|
||||||
|
It is intended to grow as additional relevant source file types are identified.
|
||||||
|
|
||||||
|
The first covered format is Matroska media that contains styled ASS/SSA
|
||||||
|
subtitle streams together with embedded font attachments.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Detecting source files that use ASS subtitle streams together with embedded
|
||||||
|
font attachments needed for correct rendering.
|
||||||
|
- Defining the required `ffx convert` behavior when this format is present.
|
||||||
|
- Preserving the required attachment streams during conversion.
|
||||||
|
- Keeping normal subtitle-track manipulation behavior for the ASS subtitle
|
||||||
|
tracks themselves.
|
||||||
|
|
||||||
|
## Out Of Scope
|
||||||
|
|
||||||
|
- General subtitle behavior for sources that do not carry this pattern.
|
||||||
|
- A complete catalog of all source file formats FFX may support later.
|
||||||
|
|
||||||
|
## Terms
|
||||||
|
|
||||||
|
- `styled ASS source`: a source media file that contains one or more subtitle
|
||||||
|
streams with `codec_type="subtitle"` and `codec_name="ass"` together with
|
||||||
|
one or more font-bearing attachment streams.
|
||||||
|
- `font attachment`: an attachment stream whose metadata identifies a font
|
||||||
|
payload, commonly through `tags.mimetype` and attachment filename metadata.
|
||||||
|
- `external subtitle feed`: subtitle tracks supplied from separate subtitle
|
||||||
|
files through the existing subtitle-import path.
|
||||||
|
- `special attachment subtracks`: the embedded font attachment streams that
|
||||||
|
belong to the styled ASS source pattern.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- `SOURCE_FILE_FORMATS-0001`: The system shall recognize the styled ASS source
|
||||||
|
pattern.
|
||||||
|
- `SOURCE_FILE_FORMATS-0002`: Recognition shall not depend on fixed stream
|
||||||
|
counts, fixed stream indices, or one exact attachment count.
|
||||||
|
- `SOURCE_FILE_FORMATS-0003`: Recognition shall use the best available ffprobe
|
||||||
|
signals. For known subtitle streams this includes
|
||||||
|
`codec_type="subtitle"` together with `codec_name="ass"`.
|
||||||
|
- `SOURCE_FILE_FORMATS-0004`: Recognition of the special attachment subtracks
|
||||||
|
shall use attachment-oriented signals such as `codec_type="attachment"` and
|
||||||
|
font-identifying metadata such as `tags.mimetype="font/ttf"` when present.
|
||||||
|
- `SOURCE_FILE_FORMATS-0005`: Recognition shall tolerate known ffprobe
|
||||||
|
variation in attachment reporting, including files where attachment streams
|
||||||
|
do not expose a `codec_name` but do expose `codec_type="attachment"` and
|
||||||
|
font-identifying tags.
|
||||||
|
- `SOURCE_FILE_FORMATS-0006`: When attachment metadata varies across files,
|
||||||
|
detection shall not depend on one exact MIME string alone. Detection shall
|
||||||
|
be written so the known pattern can vary while still recognizing font
|
||||||
|
attachments.
|
||||||
|
- `SOURCE_FILE_FORMATS-0007`: When the styled ASS source pattern is detected,
|
||||||
|
`ffx convert` shall emit an operator-facing message that reports the
|
||||||
|
detection and hints that special subtitle preservation handling is being
|
||||||
|
applied.
|
||||||
|
- `SOURCE_FILE_FORMATS-0008`: When the styled ASS source pattern is present on
|
||||||
|
the source file, `ffx convert` shall not process an external subtitle feed.
|
||||||
|
The command shall stop before conversion and report an error that explains
|
||||||
|
that separate subtitle-file import is incompatible with this source format.
|
||||||
|
- `SOURCE_FILE_FORMATS-0009`: Normal manipulation of the ASS subtitle streams
|
||||||
|
themselves shall continue to work through the usual selection, ordering,
|
||||||
|
metadata, language, title, and disposition handling paths.
|
||||||
|
- `SOURCE_FILE_FORMATS-0010`: The special attachment subtracks shall be
|
||||||
|
preserved in the target media file as-is rather than transcoded,
|
||||||
|
regenerated, or replaced from external sources.
|
||||||
|
- `SOURCE_FILE_FORMATS-0011`: Preserving the special attachment subtracks
|
||||||
|
as-is includes retaining the attachment payload and the attachment metadata
|
||||||
|
required by consumers, especially attachment filename and mimetype
|
||||||
|
information.
|
||||||
|
- `SOURCE_FILE_FORMATS-0012`: This file shall remain the extension point for
|
||||||
|
additional source-file-format contracts as FFX adds support for more special
|
||||||
|
source formats.
|
||||||
|
|
||||||
|
## Acceptance
|
||||||
|
|
||||||
|
- A source file matching the observed pattern of embedded ASS subtitles plus
|
||||||
|
font attachments is recognized even when the attachment streams do not carry
|
||||||
|
a `codec_name`.
|
||||||
|
- `ffx convert` output contains a clear detection message before the actual
|
||||||
|
conversion work proceeds.
|
||||||
|
- If external subtitle import is requested for such a source file, the command
|
||||||
|
fails fast with an explicit error instead of mixing sidecar subtitles into
|
||||||
|
the job.
|
||||||
|
- Existing manipulation of the ASS subtitle tracks still works for metadata,
|
||||||
|
titles, languages, ordering, and dispositions.
|
||||||
|
- The output media preserves the required font attachment streams and their
|
||||||
|
identifying metadata needed by downstream media players.
|
||||||
67
src/ffx/attachment_format.py
Normal file
67
src/ffx/attachment_format.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
from enum import Enum
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
class AttachmentFormat(Enum):
|
||||||
|
|
||||||
|
TTF = {'identifier': 'ttf', 'format': None, 'extension': 'ttf', 'label': 'TTF'}
|
||||||
|
PNG = {'identifier': 'png', 'format': None, 'extension': 'png', 'label': 'PNG'}
|
||||||
|
|
||||||
|
UNKNOWN = {'identifier': 'unknown', 'format': None, 'extension': None, 'label': 'UNKNOWN'}
|
||||||
|
|
||||||
|
def identifier(self):
|
||||||
|
return str(self.value['identifier'])
|
||||||
|
|
||||||
|
def label(self):
|
||||||
|
return str(self.value['label'])
|
||||||
|
|
||||||
|
def format(self):
|
||||||
|
return self.value['format']
|
||||||
|
|
||||||
|
def extension(self):
|
||||||
|
return str(self.value['extension'])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def identify(identifier: str):
|
||||||
|
formats = [f for f in AttachmentFormat if f.value['identifier'] == str(identifier)]
|
||||||
|
if formats:
|
||||||
|
return formats[0]
|
||||||
|
return AttachmentFormat.UNKNOWN
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def identifyFfprobeStream(streamObj: dict):
|
||||||
|
identifier = streamObj.get("codec_name")
|
||||||
|
identifiedFormat = AttachmentFormat.identify(identifier)
|
||||||
|
if identifiedFormat != AttachmentFormat.UNKNOWN:
|
||||||
|
return identifiedFormat
|
||||||
|
|
||||||
|
if str(streamObj.get("codec_type", "")).strip() != "attachment":
|
||||||
|
return AttachmentFormat.UNKNOWN
|
||||||
|
|
||||||
|
tags = streamObj.get("tags", {}) or {}
|
||||||
|
mimetype = str(tags.get("mimetype", "")).strip().lower()
|
||||||
|
filename = str(tags.get("filename", "")).strip().lower()
|
||||||
|
filenameExtension = os.path.splitext(filename)[1]
|
||||||
|
|
||||||
|
if (
|
||||||
|
mimetype in {
|
||||||
|
"font/ttf",
|
||||||
|
"application/x-truetype-font",
|
||||||
|
"application/x-font-ttf",
|
||||||
|
}
|
||||||
|
or "truetype" in mimetype
|
||||||
|
or filenameExtension == ".ttf"
|
||||||
|
):
|
||||||
|
return AttachmentFormat.TTF
|
||||||
|
|
||||||
|
if mimetype in {"image/png", "image/x-png"} or filenameExtension == ".png":
|
||||||
|
return AttachmentFormat.PNG
|
||||||
|
|
||||||
|
return AttachmentFormat.UNKNOWN
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fromTrackCodec(trackCodec):
|
||||||
|
identifier = getattr(trackCodec, "identifier", None)
|
||||||
|
if callable(identifier):
|
||||||
|
return AttachmentFormat.identify(trackCodec.identifier())
|
||||||
|
return AttachmentFormat.UNKNOWN
|
||||||
@@ -639,6 +639,7 @@ def getUnmuxSequence(trackDescriptor: TrackDescriptor, sourcePath, targetPrefix,
|
|||||||
|
|
||||||
trackType = trackDescriptor.getType()
|
trackType = trackDescriptor.getType()
|
||||||
trackCodec = trackDescriptor.getCodec()
|
trackCodec = trackDescriptor.getCodec()
|
||||||
|
trackFormat = trackDescriptor.getFormatDescriptor()
|
||||||
|
|
||||||
targetPathBase = os.path.join(targetDirectory, targetPrefix) if targetDirectory else targetPrefix
|
targetPathBase = os.path.join(targetDirectory, targetPrefix) if targetDirectory else targetPrefix
|
||||||
|
|
||||||
@@ -651,12 +652,12 @@ def getUnmuxSequence(trackDescriptor: TrackDescriptor, sourcePath, targetPrefix,
|
|||||||
commandTokens += ['-c', 'copy']
|
commandTokens += ['-c', 'copy']
|
||||||
|
|
||||||
# output format
|
# output format
|
||||||
codecFormat = trackCodec.format()
|
codecFormat = trackFormat.format()
|
||||||
if codecFormat is not None:
|
if codecFormat is not None:
|
||||||
commandTokens += ['-f', codecFormat]
|
commandTokens += ['-f', codecFormat]
|
||||||
|
|
||||||
# output filename
|
# output filename
|
||||||
commandTokens += [f"{targetPathBase}.{trackCodec.extension()}"]
|
commandTokens += [f"{targetPathBase}.{trackFormat.extension()}"]
|
||||||
|
|
||||||
return commandTokens
|
return commandTokens
|
||||||
|
|
||||||
@@ -771,7 +772,7 @@ def unmux(ctx,
|
|||||||
if not ctx.obj['dry_run']:
|
if not ctx.obj['dry_run']:
|
||||||
|
|
||||||
#TODO #425: Codec Enum
|
#TODO #425: Codec Enum
|
||||||
ctx.obj['logger'].info(f"Unmuxing stream {trackDescriptor.getIndex()} into file {targetPrefix}.{trackDescriptor.getCodec().extension()}")
|
ctx.obj['logger'].info(f"Unmuxing stream {trackDescriptor.getIndex()} into file {targetPrefix}.{trackDescriptor.getFormatDescriptor().extension()}")
|
||||||
|
|
||||||
ctx.obj['logger'].debug(f"Executing unmuxing sequence")
|
ctx.obj['logger'].debug(f"Executing unmuxing sequence")
|
||||||
|
|
||||||
@@ -1313,10 +1314,12 @@ def convert(ctx,
|
|||||||
sourceMediaDescriptor = mediaFileProperties.getMediaDescriptor()
|
sourceMediaDescriptor = mediaFileProperties.getMediaDescriptor()
|
||||||
|
|
||||||
|
|
||||||
|
from ffx.attachment_format import AttachmentFormat
|
||||||
|
|
||||||
if ([smd for smd in sourceMediaDescriptor.getSubtitleTracks()
|
if ([smd for smd in sourceMediaDescriptor.getSubtitleTracks()
|
||||||
if smd.getCodec() == TrackCodec.ASS]
|
if smd.getCodec() == TrackCodec.ASS]
|
||||||
and [amd for amd in sourceMediaDescriptor.getAttachmentTracks()
|
and [amd for amd in sourceMediaDescriptor.getAttachmentTracks()
|
||||||
if amd.getCodec() == TrackCodec.TTF]):
|
if amd.getAttachmentFormat() == AttachmentFormat.TTF]):
|
||||||
|
|
||||||
targetFormat = ''
|
targetFormat = ''
|
||||||
targetExtension = 'mkv'
|
targetExtension = 'mkv'
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import os, re, click
|
|||||||
|
|
||||||
from typing import List, Self
|
from typing import List, Self
|
||||||
|
|
||||||
|
from ffx.attachment_format import AttachmentFormat
|
||||||
from ffx.track_type import TrackType
|
from ffx.track_type import TrackType
|
||||||
from ffx.iso_language import IsoLanguage
|
from ffx.iso_language import IsoLanguage
|
||||||
|
|
||||||
@@ -421,11 +422,11 @@ class MediaDescriptor:
|
|||||||
|
|
||||||
if sourceMediaDescriptor:
|
if sourceMediaDescriptor:
|
||||||
fontDescriptors = [ftd for ftd in sourceMediaDescriptor.getAttachmentTracks()
|
fontDescriptors = [ftd for ftd in sourceMediaDescriptor.getAttachmentTracks()
|
||||||
if ftd.getCodec() == TrackCodec.TTF]
|
if ftd.getAttachmentFormat() == AttachmentFormat.TTF]
|
||||||
else:
|
else:
|
||||||
fontDescriptors = [ftd for ftd in self.__trackDescriptors
|
fontDescriptors = [ftd for ftd in self.__trackDescriptors
|
||||||
if ftd.getType() == TrackType.ATTACHMENT
|
if ftd.getType() == TrackType.ATTACHMENT
|
||||||
and ftd.getCodec() == TrackCodec.TTF]
|
and ftd.getAttachmentFormat() == AttachmentFormat.TTF]
|
||||||
|
|
||||||
for ad in sorted(fontDescriptors, key=lambda d: d.getIndex()):
|
for ad in sorted(fontDescriptors, key=lambda d: d.getIndex()):
|
||||||
inputMappingTokens += ["-map", f"0:{ad.getIndex()}"]
|
inputMappingTokens += ["-map", f"0:{ad.getIndex()}"]
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ class MediaWorkflowScreenBase(Screen):
|
|||||||
trackDescriptor.getIndex(),
|
trackDescriptor.getIndex(),
|
||||||
t(trackType.label()),
|
t(trackType.label()),
|
||||||
typeCounter[trackType],
|
typeCounter[trackType],
|
||||||
trackDescriptor.getCodec().label(),
|
trackDescriptor.getFormatDescriptor().label(),
|
||||||
t(audioLayout.label())
|
t(audioLayout.label())
|
||||||
if trackType == TrackType.AUDIO
|
if trackType == TrackType.AUDIO
|
||||||
and audioLayout != AudioLayout.LAYOUT_UNDEFINED
|
and audioLayout != AudioLayout.LAYOUT_UNDEFINED
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from sqlalchemy.orm import relationship, declarative_base, sessionmaker
|
|||||||
|
|
||||||
from .show import Base
|
from .show import Base
|
||||||
|
|
||||||
|
from ffx.attachment_format import AttachmentFormat
|
||||||
from ffx.track_type import TrackType
|
from ffx.track_type import TrackType
|
||||||
|
|
||||||
from ffx.iso_language import IsoLanguage
|
from ffx.iso_language import IsoLanguage
|
||||||
@@ -132,9 +133,16 @@ class Track(Base):
|
|||||||
|
|
||||||
if trackType in [t.label() for t in TrackType]:
|
if trackType in [t.label() for t in TrackType]:
|
||||||
|
|
||||||
|
if trackType == TrackType.ATTACHMENT.label():
|
||||||
|
storedFormatIdentifier = AttachmentFormat.identifyFfprobeStream(streamObj).identifier()
|
||||||
|
else:
|
||||||
|
storedFormatIdentifier = TrackCodec.identify(
|
||||||
|
streamObj.get(TrackDescriptor.FFPROBE_CODEC_KEY)
|
||||||
|
).identifier()
|
||||||
|
|
||||||
return cls(pattern_id = patternId,
|
return cls(pattern_id = patternId,
|
||||||
track_type = trackType,
|
track_type = trackType,
|
||||||
codec_name = streamObj[TrackDescriptor.FFPROBE_CODEC_NAME_KEY],
|
codec_name = storedFormatIdentifier,
|
||||||
disposition_flags = sum([2**t.index() for (k,v) in streamObj[TrackDescriptor.FFPROBE_DISPOSITION_KEY].items()
|
disposition_flags = sum([2**t.index() for (k,v) in streamObj[TrackDescriptor.FFPROBE_DISPOSITION_KEY].items()
|
||||||
if v and (t := TrackDisposition.find(k)) is not None]),
|
if v and (t := TrackDisposition.find(k)) is not None]),
|
||||||
audio_layout = AudioLayout.identify(streamObj))
|
audio_layout = AudioLayout.identify(streamObj))
|
||||||
@@ -153,8 +161,20 @@ class Track(Base):
|
|||||||
return TrackType.fromIndex(self.track_type)
|
return TrackType.fromIndex(self.track_type)
|
||||||
|
|
||||||
def getCodec(self) -> TrackCodec:
|
def getCodec(self) -> TrackCodec:
|
||||||
|
if self.getType() == TrackType.ATTACHMENT:
|
||||||
|
return TrackCodec.UNKNOWN
|
||||||
return TrackCodec.identify(self.codec_name)
|
return TrackCodec.identify(self.codec_name)
|
||||||
|
|
||||||
|
def getAttachmentFormat(self) -> AttachmentFormat:
|
||||||
|
if self.getType() != TrackType.ATTACHMENT:
|
||||||
|
return AttachmentFormat.UNKNOWN
|
||||||
|
return AttachmentFormat.identify(self.codec_name)
|
||||||
|
|
||||||
|
def getFormatDescriptor(self):
|
||||||
|
if self.getType() == TrackType.ATTACHMENT:
|
||||||
|
return self.getAttachmentFormat()
|
||||||
|
return self.getCodec()
|
||||||
|
|
||||||
def getIndex(self):
|
def getIndex(self):
|
||||||
return int(self.index) if self.index is not None else -1
|
return int(self.index) if self.index is not None else -1
|
||||||
|
|
||||||
@@ -206,7 +226,10 @@ class Track(Base):
|
|||||||
kwargs[TrackDescriptor.SUB_INDEX_KEY] = subIndex
|
kwargs[TrackDescriptor.SUB_INDEX_KEY] = subIndex
|
||||||
|
|
||||||
kwargs[TrackDescriptor.TRACK_TYPE_KEY] = self.getType()
|
kwargs[TrackDescriptor.TRACK_TYPE_KEY] = self.getType()
|
||||||
kwargs[TrackDescriptor.CODEC_KEY] = self.getCodec()
|
if self.getType() == TrackType.ATTACHMENT:
|
||||||
|
kwargs[TrackDescriptor.ATTACHMENT_FORMAT_KEY] = self.getAttachmentFormat()
|
||||||
|
else:
|
||||||
|
kwargs[TrackDescriptor.CODEC_KEY] = self.getCodec()
|
||||||
|
|
||||||
kwargs[TrackDescriptor.DISPOSITION_SET_KEY] = self.getDispositionSet()
|
kwargs[TrackDescriptor.DISPOSITION_SET_KEY] = self.getDispositionSet()
|
||||||
kwargs[TrackDescriptor.TAGS_KEY] = self.getTags()
|
kwargs[TrackDescriptor.TAGS_KEY] = self.getTags()
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ class PatternController:
|
|||||||
def _build_track_row(self, trackDescriptor: TrackDescriptor) -> Track:
|
def _build_track_row(self, trackDescriptor: TrackDescriptor) -> Track:
|
||||||
track = Track(
|
track = Track(
|
||||||
track_type=int(trackDescriptor.getType().index()),
|
track_type=int(trackDescriptor.getType().index()),
|
||||||
codec_name=str(trackDescriptor.getCodec().identifier()),
|
codec_name=str(trackDescriptor.getFormatDescriptor().identifier()),
|
||||||
index=int(trackDescriptor.getIndex()),
|
index=int(trackDescriptor.getIndex()),
|
||||||
source_index=int(trackDescriptor.getSourceIndex()),
|
source_index=int(trackDescriptor.getSourceIndex()),
|
||||||
disposition_flags=int(
|
disposition_flags=int(
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ class PatternDetailsScreen(Screen):
|
|||||||
row = (td.getIndex(),
|
row = (td.getIndex(),
|
||||||
t(trackType.label()),
|
t(trackType.label()),
|
||||||
typeCounter[trackType],
|
typeCounter[trackType],
|
||||||
td.getCodec().label(),
|
td.getFormatDescriptor().label(),
|
||||||
t(audioLayout.label()) if trackType == TrackType.AUDIO
|
t(audioLayout.label()) if trackType == TrackType.AUDIO
|
||||||
and audioLayout != AudioLayout.LAYOUT_UNDEFINED else ' ',
|
and audioLayout != AudioLayout.LAYOUT_UNDEFINED else ' ',
|
||||||
trackLanguage.label() if trackLanguage != IsoLanguage.UNDEFINED else ' ',
|
trackLanguage.label() if trackLanguage != IsoLanguage.UNDEFINED else ' ',
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ class TrackCodec(Enum):
|
|||||||
WEBVTT = {'identifier': 'webvtt', 'format': 'webvtt', 'extension': 'vtt' , 'label': 'WebVTT'}
|
WEBVTT = {'identifier': 'webvtt', 'format': 'webvtt', 'extension': 'vtt' , 'label': 'WebVTT'}
|
||||||
SRT = {'identifier': 'subrip', 'format': 'srt', 'extension': 'srt' , 'label': 'SRT'}
|
SRT = {'identifier': 'subrip', 'format': 'srt', 'extension': 'srt' , 'label': 'SRT'}
|
||||||
ASS = {'identifier': 'ass', 'format': 'ass', 'extension': 'ass' , 'label': 'ASS'}
|
ASS = {'identifier': 'ass', 'format': 'ass', 'extension': 'ass' , 'label': 'ASS'}
|
||||||
TTF = {'identifier': 'ttf', 'format': None, 'extension': 'ttf' , 'label': 'TTF'}
|
|
||||||
PGS = {'identifier': 'hdmv_pgs_subtitle', 'format': 'sup', 'extension': 'sup' , 'label': 'PGS'}
|
PGS = {'identifier': 'hdmv_pgs_subtitle', 'format': 'sup', 'extension': 'sup' , 'label': 'PGS'}
|
||||||
VOBSUB = {'identifier': 'dvd_subtitle', 'format': None, 'extension': 'mkv' , 'label': 'VobSub'}
|
VOBSUB = {'identifier': 'dvd_subtitle', 'format': None, 'extension': 'mkv' , 'label': 'VobSub'}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class TrackController():
|
|||||||
s = self.Session()
|
s = self.Session()
|
||||||
track = Track(pattern_id = patId,
|
track = Track(pattern_id = patId,
|
||||||
track_type = int(trackDescriptor.getType().index()),
|
track_type = int(trackDescriptor.getType().index()),
|
||||||
codec_name = str(trackDescriptor.getCodec().identifier()),
|
codec_name = str(trackDescriptor.getFormatDescriptor().identifier()),
|
||||||
index = int(trackDescriptor.getIndex()),
|
index = int(trackDescriptor.getIndex()),
|
||||||
source_index = int(trackDescriptor.getSourceIndex()),
|
source_index = int(trackDescriptor.getSourceIndex()),
|
||||||
disposition_flags = int(TrackDisposition.toFlags(trackDescriptor.getDispositionSet())),
|
disposition_flags = int(TrackDisposition.toFlags(trackDescriptor.getDispositionSet())),
|
||||||
@@ -82,7 +82,7 @@ class TrackController():
|
|||||||
track.index = int(trackDescriptor.getIndex())
|
track.index = int(trackDescriptor.getIndex())
|
||||||
|
|
||||||
track.track_type = int(trackDescriptor.getType().index())
|
track.track_type = int(trackDescriptor.getType().index())
|
||||||
track.codec_name = str(trackDescriptor.getCodec().identifier())
|
track.codec_name = str(trackDescriptor.getFormatDescriptor().identifier())
|
||||||
track.audio_layout = int(trackDescriptor.getAudioLayout().index())
|
track.audio_layout = int(trackDescriptor.getAudioLayout().index())
|
||||||
|
|
||||||
track.disposition_flags = int(TrackDisposition.toFlags(trackDescriptor.getDispositionSet()))
|
track.disposition_flags = int(TrackDisposition.toFlags(trackDescriptor.getDispositionSet()))
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from typing import Self
|
from typing import Self
|
||||||
|
|
||||||
|
from .attachment_format import AttachmentFormat
|
||||||
from .iso_language import IsoLanguage
|
from .iso_language import IsoLanguage
|
||||||
from .track_type import TrackType
|
from .track_type import TrackType
|
||||||
from .audio_layout import AudioLayout
|
from .audio_layout import AudioLayout
|
||||||
@@ -26,6 +27,7 @@ class TrackDescriptor:
|
|||||||
|
|
||||||
TRACK_TYPE_KEY = "track_type"
|
TRACK_TYPE_KEY = "track_type"
|
||||||
CODEC_KEY = "codec_name"
|
CODEC_KEY = "codec_name"
|
||||||
|
ATTACHMENT_FORMAT_KEY = "attachment_format"
|
||||||
AUDIO_LAYOUT_KEY = "audio_layout"
|
AUDIO_LAYOUT_KEY = "audio_layout"
|
||||||
|
|
||||||
FFPROBE_INDEX_KEY = "index"
|
FFPROBE_INDEX_KEY = "index"
|
||||||
@@ -110,15 +112,6 @@ class TrackDescriptor:
|
|||||||
else:
|
else:
|
||||||
self.__trackType = TrackType.UNKNOWN
|
self.__trackType = TrackType.UNKNOWN
|
||||||
|
|
||||||
if TrackDescriptor.CODEC_KEY in kwargs.keys():
|
|
||||||
if type(kwargs[TrackDescriptor.CODEC_KEY]) is not TrackCodec:
|
|
||||||
raise TypeError(
|
|
||||||
f"TrackDesciptor.__init__(): Argument {TrackDescriptor.CODEC_KEY} is required to be of type TrackCodec"
|
|
||||||
)
|
|
||||||
self.__trackCodec = kwargs[TrackDescriptor.CODEC_KEY]
|
|
||||||
else:
|
|
||||||
self.__trackCodec = TrackCodec.UNKNOWN
|
|
||||||
|
|
||||||
if TrackDescriptor.TAGS_KEY in kwargs.keys():
|
if TrackDescriptor.TAGS_KEY in kwargs.keys():
|
||||||
if type(kwargs[TrackDescriptor.TAGS_KEY]) is not dict:
|
if type(kwargs[TrackDescriptor.TAGS_KEY]) is not dict:
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
@@ -151,6 +144,34 @@ class TrackDescriptor:
|
|||||||
else:
|
else:
|
||||||
self.__audioLayout = AudioLayout.LAYOUT_UNDEFINED
|
self.__audioLayout = AudioLayout.LAYOUT_UNDEFINED
|
||||||
|
|
||||||
|
self.__trackCodec = TrackCodec.UNKNOWN
|
||||||
|
self.__attachmentFormat = AttachmentFormat.UNKNOWN
|
||||||
|
|
||||||
|
if self.__trackType == TrackType.ATTACHMENT:
|
||||||
|
if TrackDescriptor.ATTACHMENT_FORMAT_KEY in kwargs.keys():
|
||||||
|
if type(kwargs[TrackDescriptor.ATTACHMENT_FORMAT_KEY]) is not AttachmentFormat:
|
||||||
|
raise TypeError(
|
||||||
|
f"TrackDesciptor.__init__(): Argument {TrackDescriptor.ATTACHMENT_FORMAT_KEY} is required to be of type AttachmentFormat"
|
||||||
|
)
|
||||||
|
self.__attachmentFormat = kwargs[TrackDescriptor.ATTACHMENT_FORMAT_KEY]
|
||||||
|
elif TrackDescriptor.CODEC_KEY in kwargs.keys():
|
||||||
|
legacyCodec = kwargs[TrackDescriptor.CODEC_KEY]
|
||||||
|
if type(legacyCodec) is AttachmentFormat:
|
||||||
|
self.__attachmentFormat = legacyCodec
|
||||||
|
elif type(legacyCodec) is TrackCodec:
|
||||||
|
self.__attachmentFormat = AttachmentFormat.fromTrackCodec(legacyCodec)
|
||||||
|
else:
|
||||||
|
raise TypeError(
|
||||||
|
f"TrackDesciptor.__init__(): Argument {TrackDescriptor.CODEC_KEY} is required to be of type TrackCodec for legacy attachment compatibility"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if TrackDescriptor.CODEC_KEY in kwargs.keys():
|
||||||
|
if type(kwargs[TrackDescriptor.CODEC_KEY]) is not TrackCodec:
|
||||||
|
raise TypeError(
|
||||||
|
f"TrackDesciptor.__init__(): Argument {TrackDescriptor.CODEC_KEY} is required to be of type TrackCodec"
|
||||||
|
)
|
||||||
|
self.__trackCodec = kwargs[TrackDescriptor.CODEC_KEY]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def fromFfprobe(cls, streamObj, subIndex: int = -1):
|
def fromFfprobe(cls, streamObj, subIndex: int = -1):
|
||||||
"""Processes ffprobe stream data as array with elements according to the following example
|
"""Processes ffprobe stream data as array with elements according to the following example
|
||||||
@@ -215,7 +236,12 @@ class TrackDescriptor:
|
|||||||
|
|
||||||
kwargs[TrackDescriptor.TRACK_TYPE_KEY] = trackType
|
kwargs[TrackDescriptor.TRACK_TYPE_KEY] = trackType
|
||||||
|
|
||||||
kwargs[TrackDescriptor.CODEC_KEY] = TrackCodec.identify(streamObj[TrackDescriptor.FFPROBE_CODEC_KEY])
|
if trackType == TrackType.ATTACHMENT:
|
||||||
|
kwargs[TrackDescriptor.ATTACHMENT_FORMAT_KEY] = AttachmentFormat.identifyFfprobeStream(streamObj)
|
||||||
|
else:
|
||||||
|
kwargs[TrackDescriptor.CODEC_KEY] = TrackCodec.identify(
|
||||||
|
streamObj.get(TrackDescriptor.FFPROBE_CODEC_KEY)
|
||||||
|
)
|
||||||
|
|
||||||
kwargs[TrackDescriptor.DISPOSITION_SET_KEY] = (
|
kwargs[TrackDescriptor.DISPOSITION_SET_KEY] = (
|
||||||
{
|
{
|
||||||
@@ -277,6 +303,14 @@ class TrackDescriptor:
|
|||||||
def getCodec(self) -> TrackCodec:
|
def getCodec(self) -> TrackCodec:
|
||||||
return self.__trackCodec
|
return self.__trackCodec
|
||||||
|
|
||||||
|
def getAttachmentFormat(self) -> AttachmentFormat:
|
||||||
|
return self.__attachmentFormat
|
||||||
|
|
||||||
|
def getFormatDescriptor(self):
|
||||||
|
if self.__trackType == TrackType.ATTACHMENT:
|
||||||
|
return self.__attachmentFormat
|
||||||
|
return self.__trackCodec
|
||||||
|
|
||||||
def getLanguage(self):
|
def getLanguage(self):
|
||||||
if "language" in self.__trackTags.keys():
|
if "language" in self.__trackTags.keys():
|
||||||
return IsoLanguage.findThreeLetter(self.__trackTags["language"])
|
return IsoLanguage.findThreeLetter(self.__trackTags["language"])
|
||||||
@@ -353,12 +387,16 @@ class TrackDescriptor:
|
|||||||
TrackDescriptor.SOURCE_INDEX_KEY: int(self.__sourceIndex),
|
TrackDescriptor.SOURCE_INDEX_KEY: int(self.__sourceIndex),
|
||||||
TrackDescriptor.SUB_INDEX_KEY: int(self.__subIndex),
|
TrackDescriptor.SUB_INDEX_KEY: int(self.__subIndex),
|
||||||
TrackDescriptor.TRACK_TYPE_KEY: self.__trackType,
|
TrackDescriptor.TRACK_TYPE_KEY: self.__trackType,
|
||||||
TrackDescriptor.CODEC_KEY: self.__trackCodec,
|
|
||||||
TrackDescriptor.TAGS_KEY: dict(self.__trackTags),
|
TrackDescriptor.TAGS_KEY: dict(self.__trackTags),
|
||||||
TrackDescriptor.DISPOSITION_SET_KEY: set(self.__dispositionSet),
|
TrackDescriptor.DISPOSITION_SET_KEY: set(self.__dispositionSet),
|
||||||
TrackDescriptor.AUDIO_LAYOUT_KEY: self.__audioLayout,
|
TrackDescriptor.AUDIO_LAYOUT_KEY: self.__audioLayout,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.__trackType == TrackType.ATTACHMENT:
|
||||||
|
kwargs[TrackDescriptor.ATTACHMENT_FORMAT_KEY] = self.__attachmentFormat
|
||||||
|
else:
|
||||||
|
kwargs[TrackDescriptor.CODEC_KEY] = self.__trackCodec
|
||||||
|
|
||||||
if context is not None:
|
if context is not None:
|
||||||
kwargs[TrackDescriptor.CONTEXT_KEY] = context
|
kwargs[TrackDescriptor.CONTEXT_KEY] = context
|
||||||
elif self.__context:
|
elif self.__context:
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from textual.widgets import Header, Footer, Static, Button, SelectionList, Selec
|
|||||||
from textual.containers import Grid
|
from textual.containers import Grid
|
||||||
from textual.widgets._data_table import CellDoesNotExist
|
from textual.widgets._data_table import CellDoesNotExist
|
||||||
|
|
||||||
|
from .attachment_format import AttachmentFormat
|
||||||
from .audio_layout import AudioLayout
|
from .audio_layout import AudioLayout
|
||||||
from .iso_language import IsoLanguage
|
from .iso_language import IsoLanguage
|
||||||
from .tag_delete_screen import TagDeleteScreen
|
from .tag_delete_screen import TagDeleteScreen
|
||||||
@@ -141,6 +142,7 @@ class TrackDetailsScreen(Screen):
|
|||||||
if self.__isNew:
|
if self.__isNew:
|
||||||
self.__trackType = trackType
|
self.__trackType = trackType
|
||||||
self.__trackCodec = TrackCodec.UNKNOWN
|
self.__trackCodec = TrackCodec.UNKNOWN
|
||||||
|
self.__attachmentFormat = AttachmentFormat.UNKNOWN
|
||||||
self.__audioLayout = AudioLayout.LAYOUT_UNDEFINED
|
self.__audioLayout = AudioLayout.LAYOUT_UNDEFINED
|
||||||
self.__index = index
|
self.__index = index
|
||||||
self.__subIndex = subIndex
|
self.__subIndex = subIndex
|
||||||
@@ -150,6 +152,7 @@ class TrackDetailsScreen(Screen):
|
|||||||
else:
|
else:
|
||||||
self.__trackType = trackDescriptor.getType()
|
self.__trackType = trackDescriptor.getType()
|
||||||
self.__trackCodec = trackDescriptor.getCodec()
|
self.__trackCodec = trackDescriptor.getCodec()
|
||||||
|
self.__attachmentFormat = trackDescriptor.getAttachmentFormat()
|
||||||
self.__audioLayout = trackDescriptor.getAudioLayout()
|
self.__audioLayout = trackDescriptor.getAudioLayout()
|
||||||
self.__index = trackDescriptor.getIndex()
|
self.__index = trackDescriptor.getIndex()
|
||||||
self.__subIndex = trackDescriptor.getSubIndex()
|
self.__subIndex = trackDescriptor.getSubIndex()
|
||||||
@@ -433,7 +436,10 @@ class TrackDetailsScreen(Screen):
|
|||||||
if not isinstance(selectedTrackType, TrackType):
|
if not isinstance(selectedTrackType, TrackType):
|
||||||
selectedTrackType = TrackType.UNKNOWN
|
selectedTrackType = TrackType.UNKNOWN
|
||||||
kwargs[TrackDescriptor.TRACK_TYPE_KEY] = selectedTrackType
|
kwargs[TrackDescriptor.TRACK_TYPE_KEY] = selectedTrackType
|
||||||
kwargs[TrackDescriptor.CODEC_KEY] = self.__trackCodec
|
if selectedTrackType == TrackType.ATTACHMENT:
|
||||||
|
kwargs[TrackDescriptor.ATTACHMENT_FORMAT_KEY] = self.__attachmentFormat
|
||||||
|
else:
|
||||||
|
kwargs[TrackDescriptor.CODEC_KEY] = self.__trackCodec
|
||||||
|
|
||||||
if selectedTrackType == TrackType.AUDIO:
|
if selectedTrackType == TrackType.AUDIO:
|
||||||
selectedAudioLayout = self.query_one("#audio_layout_select", Select).value
|
selectedAudioLayout = self.query_one("#audio_layout_select", Select).value
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ if str(SRC_ROOT) not in sys.path:
|
|||||||
|
|
||||||
|
|
||||||
from ffx.audio_layout import AudioLayout # noqa: E402
|
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 # noqa: E402
|
||||||
from ffx.iso_language import IsoLanguage # noqa: E402
|
from ffx.iso_language import IsoLanguage # noqa: E402
|
||||||
from ffx.logging_utils import get_ffx_logger # noqa: E402
|
from ffx.logging_utils import get_ffx_logger # noqa: E402
|
||||||
@@ -200,6 +201,32 @@ class TagTableScreenStateTests(unittest.TestCase):
|
|||||||
self.assertEqual("German Audio", descriptor.getTitle())
|
self.assertEqual("German Audio", descriptor.getTitle())
|
||||||
self.assertEqual("value", descriptor.getTags()["KEEP"])
|
self.assertEqual("value", descriptor.getTags()["KEEP"])
|
||||||
|
|
||||||
|
def test_track_details_screen_preserves_attachment_format_for_attachment_tracks(self):
|
||||||
|
screen = object.__new__(TrackDetailsScreen)
|
||||||
|
screen.context = {"logger": get_ffx_logger()}
|
||||||
|
screen._TrackDetailsScreen__trackDescriptor = None
|
||||||
|
screen._TrackDetailsScreen__patternId = 5
|
||||||
|
screen._TrackDetailsScreen__index = 4
|
||||||
|
screen._TrackDetailsScreen__subIndex = 0
|
||||||
|
screen._TrackDetailsScreen__trackCodec = TrackCodec.UNKNOWN
|
||||||
|
screen._TrackDetailsScreen__attachmentFormat = AttachmentFormat.TTF
|
||||||
|
screen._TrackDetailsScreen__draftTrackTags = {"filename": "font.ttf", "mimetype": "font/ttf"}
|
||||||
|
|
||||||
|
widgets = {
|
||||||
|
"#type_select": FakeValueWidget(TrackType.ATTACHMENT),
|
||||||
|
"#audio_layout_select": FakeValueWidget(AudioLayout.LAYOUT_UNDEFINED),
|
||||||
|
"#language_select": FakeValueWidget(Select.NULL),
|
||||||
|
"#title_input": FakeInputWidget(""),
|
||||||
|
"#dispositions_selection_list": FakeSelectionListWidget(set()),
|
||||||
|
}
|
||||||
|
screen.query_one = lambda selector, _widget_type=None: widgets[selector]
|
||||||
|
|
||||||
|
descriptor = screen.getTrackDescriptorFromInput()
|
||||||
|
|
||||||
|
self.assertEqual(TrackType.ATTACHMENT, descriptor.getType())
|
||||||
|
self.assertEqual(AttachmentFormat.TTF, descriptor.getAttachmentFormat())
|
||||||
|
self.assertEqual(TrackCodec.UNKNOWN, descriptor.getCodec())
|
||||||
|
|
||||||
def test_track_details_screen_auto_sets_localized_title_from_selected_language(self):
|
def test_track_details_screen_auto_sets_localized_title_from_selected_language(self):
|
||||||
set_current_language("de")
|
set_current_language("de")
|
||||||
screen = object.__new__(TrackDetailsScreen)
|
screen = object.__new__(TrackDetailsScreen)
|
||||||
|
|||||||
87
tests/unit/test_track_descriptor_probe.py
Normal file
87
tests/unit/test_track_descriptor_probe.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
SRC_ROOT = Path(__file__).resolve().parents[2] / "src"
|
||||||
|
|
||||||
|
if str(SRC_ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(SRC_ROOT))
|
||||||
|
|
||||||
|
|
||||||
|
from ffx.attachment_format import AttachmentFormat # noqa: E402
|
||||||
|
from ffx.media_descriptor import MediaDescriptor # noqa: E402
|
||||||
|
from ffx.track_codec import TrackCodec # noqa: E402
|
||||||
|
from ffx.track_descriptor import TrackDescriptor # noqa: E402
|
||||||
|
from ffx.track_type import TrackType # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
ASSETS_ROOT = Path(__file__).resolve().parents[1] / "assets"
|
||||||
|
|
||||||
|
|
||||||
|
class TrackDescriptorProbeTests(unittest.TestCase):
|
||||||
|
def test_attachment_without_codec_name_uses_font_metadata_to_identify_ttf(self):
|
||||||
|
descriptor = TrackDescriptor.fromFfprobe(
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"codec_type": "attachment",
|
||||||
|
"disposition": {"default": 0},
|
||||||
|
"tags": {
|
||||||
|
"filename": "AmazonEmberTanuki-Italic.ttf",
|
||||||
|
"mimetype": "font/ttf",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
subIndex=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIsNotNone(descriptor)
|
||||||
|
self.assertEqual(TrackType.ATTACHMENT, descriptor.getType())
|
||||||
|
self.assertEqual(AttachmentFormat.TTF, descriptor.getAttachmentFormat())
|
||||||
|
self.assertEqual(AttachmentFormat.TTF, descriptor.getFormatDescriptor())
|
||||||
|
self.assertEqual(TrackCodec.UNKNOWN, descriptor.getCodec())
|
||||||
|
|
||||||
|
def test_attachment_without_codec_name_still_probes_as_unknown_when_not_font(self):
|
||||||
|
descriptor = TrackDescriptor.fromFfprobe(
|
||||||
|
{
|
||||||
|
"index": 9,
|
||||||
|
"codec_type": "attachment",
|
||||||
|
"disposition": {"default": 0},
|
||||||
|
"tags": {
|
||||||
|
"filename": "cover.bin",
|
||||||
|
"mimetype": "application/octet-stream",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
subIndex=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIsNotNone(descriptor)
|
||||||
|
self.assertEqual(TrackType.ATTACHMENT, descriptor.getType())
|
||||||
|
self.assertEqual(AttachmentFormat.UNKNOWN, descriptor.getAttachmentFormat())
|
||||||
|
self.assertEqual(TrackCodec.UNKNOWN, descriptor.getCodec())
|
||||||
|
|
||||||
|
def test_media_descriptor_from_boruto_probe_json_handles_attachment_streams_without_codec_name(self):
|
||||||
|
probe_payload = json.loads(
|
||||||
|
(ASSETS_ROOT / "ffprobe.out.json").read_text(encoding="utf-8")
|
||||||
|
)
|
||||||
|
|
||||||
|
descriptor = MediaDescriptor.fromFfprobe(
|
||||||
|
{"logger": None},
|
||||||
|
probe_payload["format"],
|
||||||
|
probe_payload["streams"],
|
||||||
|
)
|
||||||
|
|
||||||
|
track_descriptors = descriptor.getTrackDescriptors()
|
||||||
|
attachment_tracks = descriptor.getAttachmentTracks()
|
||||||
|
|
||||||
|
self.assertEqual(14, len(track_descriptors))
|
||||||
|
self.assertEqual(10, len(attachment_tracks))
|
||||||
|
self.assertTrue(
|
||||||
|
all(track.getAttachmentFormat() == AttachmentFormat.TTF for track in attachment_tracks)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user