prep 0.2.6

This commit is contained in:
Javanaut
2026-04-13 20:04:25 +02:00
parent e614ca5d75
commit 037388886e
49 changed files with 5702 additions and 490 deletions

View File

@@ -49,6 +49,9 @@ class ConfigureWorkstationScriptTests(unittest.TestCase):
"HOME": str(self.home_dir),
"PATH": f"{self.stub_bin_dir}:{os.environ.get('PATH', '')}",
"FFX_PYTHON": str(BUNDLE_PYTHON),
"LANG": "C.UTF-8",
"LC_ALL": "C.UTF-8",
"LC_MESSAGES": "C.UTF-8",
**env_overrides,
}
@@ -76,6 +79,7 @@ class ConfigureWorkstationScriptTests(unittest.TestCase):
self.assertEqual(
{
"databasePath": str(self.home_dir / ".local" / "var" / "ffx" / "ffx.db"),
"language": "de",
"logDirectory": str(self.home_dir / ".local" / "var" / "log"),
"subtitlesDirectory": str(
self.home_dir / ".local" / "var" / "sync" / "subtitles"
@@ -113,6 +117,24 @@ class ConfigureWorkstationScriptTests(unittest.TestCase):
config_data,
)
def test_script_seeds_system_language_into_default_config(self):
completed = self.run_script(
LANG="fr_FR.UTF-8",
LC_ALL="fr_FR.UTF-8",
LC_MESSAGES="fr_FR.UTF-8",
)
self.assertEqual(
0,
completed.returncode,
f"STDOUT:\n{completed.stdout}\nSTDERR:\n{completed.stderr}",
)
config_path = self.home_dir / ".local" / "etc" / "ffx.json"
config_data = json.loads(config_path.read_text(encoding="utf-8"))
self.assertEqual("fr", config_data["language"])
def test_script_honors_custom_template_override(self):
custom_template_path = Path(self.tempdir.name) / "custom-config.j2"
custom_template_path.write_text(

89
tests/unit/test_i18n.py Normal file
View File

@@ -0,0 +1,89 @@
from __future__ import annotations
from pathlib import Path
import json
import sys
import tempfile
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.i18n import ( # noqa: E402
detect_system_language,
read_configured_language,
resolve_application_language,
set_current_language,
t,
)
from ffx.iso_language import IsoLanguage # noqa: E402
class I18nTests(unittest.TestCase):
def tearDown(self):
set_current_language("de")
def test_cli_language_takes_precedence_over_config_and_system(self):
self.assertEqual(
"es",
resolve_application_language(
cli_language="es",
config_language="fr",
system_language="ja",
),
)
def test_config_language_takes_precedence_over_system(self):
self.assertEqual(
"fr",
resolve_application_language(
config_language="fr",
system_language="ja",
),
)
def test_system_language_is_used_when_no_cli_or_config_is_present(self):
self.assertEqual("ja", resolve_application_language(system_language="ja"))
def test_german_is_default_when_no_supported_language_is_available(self):
self.assertEqual(
"de",
resolve_application_language(
env={
"LANG": "C.UTF-8",
"LC_ALL": "C.UTF-8",
"LC_MESSAGES": "C.UTF-8",
}
),
)
def test_system_language_detection_normalizes_norwegian_bokmal(self):
self.assertEqual(
"nb",
detect_system_language({"LANG": "nb_NO.UTF-8"}),
)
def test_read_configured_language_normalizes_language_code(self):
with tempfile.TemporaryDirectory() as tempdir:
config_path = Path(tempdir) / "ffx.json"
config_path.write_text(
json.dumps({"language": "pt_BR.UTF-8"}),
encoding="utf-8",
)
self.assertEqual("pt", read_configured_language(config_path))
def test_phrase_translation_uses_catalog_for_selected_language(self):
set_current_language("fr")
self.assertEqual("Ajouter", t("Add"))
def test_iso_language_labels_use_catalog_for_selected_language(self):
set_current_language("de")
self.assertEqual("Deutsch", IsoLanguage.GERMAN.label())
if __name__ == "__main__":
unittest.main()

View File

@@ -15,6 +15,7 @@ from ffx.media_descriptor import MediaDescriptor # noqa: E402
from ffx.media_descriptor_change_set import MediaDescriptorChangeSet # 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
from ffx.logging_utils import get_ffx_logger # noqa: E402
@@ -27,6 +28,9 @@ class StaticConfig:
class MediaDescriptorChangeSetTests(unittest.TestCase):
def tearDown(self):
set_current_language("de")
def test_non_primary_source_language_code_is_normalized_in_changed_track_metadata(self):
context = {
"logger": get_ffx_logger(),
@@ -171,6 +175,111 @@ class MediaDescriptorChangeSetTests(unittest.TestCase):
self.assertIn("language=deu", metadata_tokens)
self.assertNotIn("language=ger", metadata_tokens)
def test_subtitle_without_title_gets_language_name_when_normalization_enabled(self):
set_current_language("de")
context = {
"logger": get_ffx_logger(),
"config": StaticConfig({}),
}
source_track = TrackDescriptor(
index=0,
source_index=0,
sub_index=0,
track_type=TrackType.SUBTITLE,
tags={"language": "ger"},
)
target_track = TrackDescriptor(
index=0,
source_index=0,
sub_index=0,
track_type=TrackType.SUBTITLE,
tags={"language": "ger"},
)
change_set = MediaDescriptorChangeSet(
context,
MediaDescriptor(track_descriptors=[target_track]),
MediaDescriptor(track_descriptors=[source_track]),
)
metadata_tokens = change_set.generateMetadataTokens()
change_set_obj = change_set.getChangeSetObj()
self.assertIn("-metadata:s:s:0", metadata_tokens)
self.assertIn("language=deu", metadata_tokens)
self.assertIn("title=Deutsch", metadata_tokens)
self.assertEqual(
"Deutsch",
change_set_obj["tracks"]["changed"][0]["tags"]["added"]["title"],
)
def test_subtitle_without_title_uses_current_language_for_generated_title(self):
set_current_language("en")
context = {
"logger": get_ffx_logger(),
"config": StaticConfig({}),
}
source_track = TrackDescriptor(
index=0,
source_index=0,
sub_index=0,
track_type=TrackType.SUBTITLE,
tags={"language": "ger"},
)
target_track = TrackDescriptor(
index=0,
source_index=0,
sub_index=0,
track_type=TrackType.SUBTITLE,
tags={"language": "ger"},
)
change_set = MediaDescriptorChangeSet(
context,
MediaDescriptor(track_descriptors=[target_track]),
MediaDescriptor(track_descriptors=[source_track]),
)
metadata_tokens = change_set.generateMetadataTokens()
self.assertIn("title=German", metadata_tokens)
self.assertNotIn("title=Deutsch", metadata_tokens)
def test_non_subtitle_track_without_title_does_not_get_language_name(self):
context = {
"logger": get_ffx_logger(),
"config": StaticConfig({}),
}
source_track = TrackDescriptor(
index=0,
source_index=0,
sub_index=0,
track_type=TrackType.AUDIO,
tags={"language": "ger"},
)
target_track = TrackDescriptor(
index=0,
source_index=0,
sub_index=0,
track_type=TrackType.AUDIO,
tags={"language": "ger"},
)
change_set = MediaDescriptorChangeSet(
context,
MediaDescriptor(track_descriptors=[target_track]),
MediaDescriptor(track_descriptors=[source_track]),
)
metadata_tokens = change_set.generateMetadataTokens()
self.assertIn("-metadata:s:a:0", metadata_tokens)
self.assertIn("language=deu", metadata_tokens)
self.assertNotIn("title=Deutsch", metadata_tokens)
def test_target_only_tracks_still_emit_remove_tokens_for_configured_stream_keys(self):
context = {
"logger": get_ffx_logger(),
@@ -259,6 +368,32 @@ class MediaDescriptorChangeSetTests(unittest.TestCase):
self.assertNotIn("creation_time=", metadata_tokens)
self.assertNotIn("BPS=", metadata_tokens)
def test_normalization_can_be_disabled_per_context(self):
context = {
"logger": get_ffx_logger(),
"config": StaticConfig({}),
"apply_metadata_normalization": False,
}
target_track = TrackDescriptor(
index=0,
source_index=0,
sub_index=0,
track_type=TrackType.AUDIO,
tags={"language": "ger", "title": "German Main"},
)
change_set = MediaDescriptorChangeSet(
context,
MediaDescriptor(track_descriptors=[target_track]),
)
metadata_tokens = change_set.generateMetadataTokens()
self.assertIn("-metadata:s:a:0", metadata_tokens)
self.assertIn("language=ger", metadata_tokens)
self.assertNotIn("language=deu", metadata_tokens)
if __name__ == "__main__":
unittest.main()

View File

@@ -38,6 +38,7 @@ def make_context(*, dry_run: bool = False) -> dict:
"config": StaticConfig(),
"dry_run": dry_run,
"apply_metadata_cleanup": True,
"apply_metadata_normalization": True,
}
@@ -65,6 +66,8 @@ class MetadataEditorTests(unittest.TestCase):
self.assertFalse(context["perform_cut"])
self.assertTrue(context["no_signature"])
self.assertEqual({}, context["encoding_metadata_tags"])
self.assertTrue(context["apply_metadata_cleanup"])
self.assertTrue(context["apply_metadata_normalization"])
def test_create_temporary_output_path_uses_same_directory_and_extension(self):
with tempfile.TemporaryDirectory() as tmpdir:

View File

@@ -13,6 +13,7 @@ if str(SRC_ROOT) not in sys.path:
from ffx import screen_support # noqa: E402
from ffx.i18n import set_current_language, t # noqa: E402
class StaticConfig:
@@ -38,7 +39,28 @@ class FakeTagTable:
return row_key
class FakeApp:
def __init__(self, screen_stack):
self.screen_stack = list(screen_stack)
self.pop_called = False
self.exit_called = False
def pop_screen(self):
self.pop_called = True
def exit(self):
self.exit_called = True
class FakeScreen:
def __init__(self, screen_stack):
self.app = FakeApp(screen_stack)
class ScreenSupportTests(unittest.TestCase):
def tearDown(self):
set_current_language("de")
def make_context(self):
return {
"config": StaticConfig(
@@ -122,6 +144,30 @@ class ScreenSupportTests(unittest.TestCase):
table.rows["row-1"],
)
def test_go_back_or_exit_exits_from_first_pushed_screen(self):
screen = FakeScreen(screen_stack=["base", "shows"])
screen_support.go_back_or_exit(screen)
self.assertFalse(screen.app.pop_called)
self.assertTrue(screen.app.exit_called)
def test_go_back_or_exit_pops_nested_screen(self):
screen = FakeScreen(screen_stack=["base", "shows", "details"])
screen_support.go_back_or_exit(screen)
self.assertTrue(screen.app.pop_called)
self.assertFalse(screen.app.exit_called)
def test_localized_column_width_handles_combining_character_labels(self):
set_current_language("ta")
translated = t("SubIndex")
self.assertEqual("துணைச்சுட்டி", translated)
self.assertGreater(len(translated), 8)
self.assertEqual(len(translated) + 2, screen_support.localized_column_width(translated, 8))
if __name__ == "__main__":
unittest.main()

View File

@@ -4,6 +4,8 @@ from pathlib import Path
import sys
import unittest
from textual.widgets import Select
SRC_ROOT = Path(__file__).resolve().parents[2] / "src"
@@ -15,6 +17,7 @@ from ffx.audio_layout import AudioLayout # 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_edit_screen import MediaEditScreen # noqa: E402
from ffx.pattern_details_screen import PatternDetailsScreen # noqa: E402
from ffx.show_descriptor import ShowDescriptor # noqa: E402
@@ -29,15 +32,30 @@ from ffx.track_type import TrackType # noqa: E402
class FakeTagTable:
def __init__(self):
self.rows = {}
self.columns = []
self.cursor_coordinate = (0, 0)
self._selected_row_key = None
self._next_index = 0
self._row_order = []
def clear(self):
def clear(self, columns=False):
self.rows.clear()
self._selected_row_key = None
self._row_order.clear()
if columns:
self.columns.clear()
def add_column(self, label, *, width=None, key=None, default=None):
column_key = key if key is not None else len(self.columns)
self.columns.append(
{
"key": column_key,
"label": label,
"width": width,
"default": default,
}
)
return column_key
def add_row(self, *values):
row_key = f"row-{self._next_index}"
@@ -112,6 +130,9 @@ def make_show_descriptor(show_id, name="Show", year=2000):
class TagTableScreenStateTests(unittest.TestCase):
def tearDown(self):
set_current_language("de")
def test_track_details_screen_reads_selected_tag_from_raw_row_mapping(self):
screen = object.__new__(TrackDetailsScreen)
screen.trackTagsTable = FakeTagTable()
@@ -161,6 +182,88 @@ class TagTableScreenStateTests(unittest.TestCase):
self.assertEqual("German Audio", descriptor.getTitle())
self.assertEqual("value", descriptor.getTags()["KEEP"])
def test_track_details_screen_auto_sets_localized_title_from_selected_language(self):
set_current_language("de")
screen = object.__new__(TrackDetailsScreen)
screen._TrackDetailsScreen__titleAutoManaged = True
screen._TrackDetailsScreen__suppressTitleChanged = False
screen._TrackDetailsScreen__lastAutoTitle = ""
widgets = {
"#language_select": FakeValueWidget(IsoLanguage.UNDEFINED),
"#title_input": FakeInputWidget(""),
}
screen.query_one = lambda selector, _widget_type=None: widgets[selector]
widgets["#language_select"].value = IsoLanguage.GERMAN
screen._handle_language_selection_changed(IsoLanguage.GERMAN)
self.assertEqual("Deutsch", widgets["#title_input"].value)
widgets["#language_select"].value = IsoLanguage.ENGLISH
screen._handle_language_selection_changed(IsoLanguage.ENGLISH)
self.assertEqual("Englisch", widgets["#title_input"].value)
def test_track_details_screen_auto_title_stops_after_manual_title_change(self):
set_current_language("de")
screen = object.__new__(TrackDetailsScreen)
screen._TrackDetailsScreen__titleAutoManaged = True
screen._TrackDetailsScreen__suppressTitleChanged = False
screen._TrackDetailsScreen__lastAutoTitle = ""
widgets = {
"#language_select": FakeValueWidget(IsoLanguage.GERMAN),
"#title_input": FakeInputWidget(""),
}
screen.query_one = lambda selector, _widget_type=None: widgets[selector]
screen._handle_language_selection_changed(IsoLanguage.GERMAN)
self.assertEqual("Deutsch", widgets["#title_input"].value)
widgets["#title_input"].value = "Eigener Titel"
screen._handle_title_input_changed("Eigener Titel")
widgets["#language_select"].value = IsoLanguage.ENGLISH
screen._handle_language_selection_changed(IsoLanguage.ENGLISH)
self.assertEqual("Eigener Titel", widgets["#title_input"].value)
def test_track_details_screen_does_not_auto_set_title_when_helper_is_inactive(self):
set_current_language("de")
screen = object.__new__(TrackDetailsScreen)
screen._TrackDetailsScreen__titleAutoManaged = False
screen._TrackDetailsScreen__suppressTitleChanged = False
screen._TrackDetailsScreen__lastAutoTitle = ""
widgets = {
"#language_select": FakeValueWidget(IsoLanguage.UNDEFINED),
"#title_input": FakeInputWidget("Preset"),
}
screen.query_one = lambda selector, _widget_type=None: widgets[selector]
widgets["#language_select"].value = IsoLanguage.GERMAN
screen._handle_language_selection_changed(IsoLanguage.GERMAN)
self.assertEqual("Preset", widgets["#title_input"].value)
def test_track_details_screen_language_options_are_sorted_by_localized_label(self):
set_current_language("de")
language_options = TrackDetailsScreen.build_language_options()
labels = [label for label, _language in language_options]
languages = [_language for _label, _language in language_options]
self.assertEqual(labels, sorted(labels, key=str.casefold))
self.assertNotIn(IsoLanguage.UNDEFINED, languages)
def test_track_details_screen_uses_blank_select_value_for_undefined_language(self):
self.assertEqual(
TrackDetailsScreen.language_select_value(IsoLanguage.UNDEFINED),
Select.NULL,
)
self.assertEqual(
IsoLanguage.GERMAN,
TrackDetailsScreen.language_select_value(IsoLanguage.GERMAN),
)
def test_pattern_details_screen_reads_selected_track_from_row_mapping(self):
first_track = make_track_descriptor(0, 0, TrackType.VIDEO)
second_track = make_track_descriptor(1, 0, TrackType.SUBTITLE)
@@ -215,6 +318,20 @@ class TagTableScreenStateTests(unittest.TestCase):
self.assertIs(second_track, screen.getSelectedTrackDescriptor())
def test_media_edit_screen_update_tracks_rebuilds_columns_for_auto_width_recalculation(self):
first_track = make_track_descriptor(0, 0, TrackType.VIDEO)
first_track.getTags()["title"] = "A much longer updated title"
screen = object.__new__(MediaEditScreen)
screen.tracksTable = FakeTagTable()
screen._sourceMediaDescriptor = FakeMediaDescriptor([first_track])
screen._trackRowData = {}
screen.updateTracks()
self.assertEqual(9, len(screen.tracksTable.columns))
self.assertIn("A much longer updated title", screen.tracksTable.rows["row-0"])
def test_pattern_details_screen_reads_selected_shifted_season_from_row_mapping(self):
screen = object.__new__(PatternDetailsScreen)
screen.shiftedSeasonsTable = FakeTagTable()