kdk 3.2.0__tar.gz → 3.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {kdk-3.2.0/src/kdk.egg-info → kdk-3.3.1}/PKG-INFO +3 -3
- {kdk-3.2.0 → kdk-3.3.1}/README.md +2 -2
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/__init__.py +1 -1
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/kodi/piers/strings.po +64 -3
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/skin/skin.py +79 -30
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/validation/urls.py +29 -3
- {kdk-3.2.0 → kdk-3.3.1/src/kdk.egg-info}/PKG-INFO +3 -3
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_urls.py +84 -18
- {kdk-3.2.0 → kdk-3.3.1}/LICENSE +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/pyproject.toml +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/setup.cfg +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/__main__.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/config.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/core.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/addon_schema/addon.xsd +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/addon_schema/gotham_skin.xsd +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/addon_schema/matrix_metadata.xsd +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/icon.ico +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/kodi/omega/colors.xml +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/kodi/omega/strings.po +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/kodi/piers/colors.xml +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/kodi_builtin_controls.xml +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/omega/controls.xml +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/omega/data.xml +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/omega/infonames.json +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/omega/obsolete_windows.json +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/omega/windows.json +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/piers/controls.xml +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/piers/data.xml +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/piers/infonames.json +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/piers/obsolete_windows.json +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/data/piers/windows.json +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/gui/__init__.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/gui/__main__.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/gui/app.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/__init__.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/addon/__init__.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/addon/addon.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/addon/translations.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/coredata.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/infonames.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/infoprovider/__init__.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/infoprovider/checker.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/infoprovider/loader.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/infoprovider/provider.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/polib/LICENSE +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/polib/__init__.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/polib/polib.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/reporting/__init__.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/reporting/text.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/skin/__init__.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/skin/include.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/skin/index.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/skin/maps.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/skin/resolution.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/skin/resources.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/skin/textures.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/utils/__init__.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/utils/colors.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/utils/expressions.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/utils/files.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/utils/images.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/utils/infobool.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/utils/validation.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/utils/xml.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/validation/__init__.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/validation/constants.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/validation/filecheck.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/validation/font.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/validation/hierarchy.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/validation/ids.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/validation/image.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/validation/include.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/validation/interpreter.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/validation/label.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/validation/repo.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/validation/rules.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/validation/skinmap.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/validation/suppress.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/libs/validation/variable.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/output/__init__.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/output/browse.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/output/cache.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/output/compare.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/output/logs.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/output/payload.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/output/progress.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk/output/render.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk.egg-info/SOURCES.txt +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk.egg-info/dependency_links.txt +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk.egg-info/entry_points.txt +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk.egg-info/requires.txt +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/src/kdk.egg-info/top_level.txt +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_addon_type.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_archive.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_bracket_validation.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_brackets.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_circular_references.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_cli.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_color_values.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_compare.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_conditional_defaults.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_constant_resolution_accuracy.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_control_template_semantics.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_coredata.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_expression_resolution.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_expressions.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_extract_expression_at_offset.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_file_integrity.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_file_name_case.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_font_validation.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_github_output.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_hierarchy.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_id_refs.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_ids.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_image_info.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_image_validation.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_include_faults.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_include_file_refs.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_include_validation.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_includes.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_index_order.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_infobool.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_infonames.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_interpreter.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_json_output.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_kodi_resolution.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_label_attrs.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_label_validation.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_load_order.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_maps.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_muted_output.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_obsolete_windows.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_param_in_include_names.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_param_resolution.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_param_value.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_param_values.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_parse_errors.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_po_strings.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_release_gated_tags.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_repo.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_rules.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_save_report.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_singleton_tags.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_skin_string_range.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_split_top_level_commas.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_suppress.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_textures.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_utils.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_valueless_tags.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_variables.py +0 -0
- {kdk-3.2.0 → kdk-3.3.1}/tests/test_xbt.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kdk
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.3.1
|
|
4
4
|
Summary: Kodi skin validation tool - CLI and GUI
|
|
5
5
|
Author-email: MikeSiLVO <mikesilvo164@gmail.com>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -54,7 +54,7 @@ contain the same code; the GUI build runs without a console window, so
|
|
|
54
54
|
double-clicking it doesn't open a terminal. No Python install needed.
|
|
55
55
|
|
|
56
56
|
Download them from the [Releases page](https://github.com/MikeSiLVO/kdk/releases).
|
|
57
|
-
`<version>` is the release tag, such as `v3.
|
|
57
|
+
`<version>` is the release tag, such as `v3.3.1`:
|
|
58
58
|
|
|
59
59
|
- **Windows (x86_64)**: `kdk-gui-<version>-windows-x86_64.exe` and
|
|
60
60
|
`kdk-cli-<version>-windows-x86_64.exe`.
|
|
@@ -196,7 +196,7 @@ still points at the right file.
|
|
|
196
196
|
Run it on `ubuntu-latest`. Windows runners check text files out with CRLF line
|
|
197
197
|
endings, which File Integrity reports as errors.
|
|
198
198
|
|
|
199
|
-
##
|
|
199
|
+
## Muting a finding
|
|
200
200
|
|
|
201
201
|
An XML comment mutes warnings the skin has already judged, the way a
|
|
202
202
|
`# type: ignore` does in Python. `kdk-ignore-file` covers the whole file it sits
|
|
@@ -15,7 +15,7 @@ contain the same code; the GUI build runs without a console window, so
|
|
|
15
15
|
double-clicking it doesn't open a terminal. No Python install needed.
|
|
16
16
|
|
|
17
17
|
Download them from the [Releases page](https://github.com/MikeSiLVO/kdk/releases).
|
|
18
|
-
`<version>` is the release tag, such as `v3.
|
|
18
|
+
`<version>` is the release tag, such as `v3.3.1`:
|
|
19
19
|
|
|
20
20
|
- **Windows (x86_64)**: `kdk-gui-<version>-windows-x86_64.exe` and
|
|
21
21
|
`kdk-cli-<version>-windows-x86_64.exe`.
|
|
@@ -157,7 +157,7 @@ still points at the right file.
|
|
|
157
157
|
Run it on `ubuntu-latest`. Windows runners check text files out with CRLF line
|
|
158
158
|
endings, which File Integrity reports as errors.
|
|
159
159
|
|
|
160
|
-
##
|
|
160
|
+
## Muting a finding
|
|
161
161
|
|
|
162
162
|
An XML comment mutes warnings the skin has already judged, the way a
|
|
163
163
|
`# type: ignore` does in Python. `kdk-ignore-file` covers the whole file it sits
|
|
@@ -9537,7 +9537,13 @@ msgctxt "#16297"
|
|
|
9537
9537
|
msgid "Smooth"
|
|
9538
9538
|
msgstr ""
|
|
9539
9539
|
|
|
9540
|
-
|
|
9540
|
+
#. Name of the video filter category that holds the filters without a shader
|
|
9541
|
+
#: xbmc/games/dialogs/osd/DialogGameVideoFilter.cpp
|
|
9542
|
+
msgctxt "#16298"
|
|
9543
|
+
msgid "Built-in"
|
|
9544
|
+
msgstr ""
|
|
9545
|
+
|
|
9546
|
+
#empty string with id 16299
|
|
9541
9547
|
|
|
9542
9548
|
#: xbmc/video/dialogs/GUIDialogVideoSettings.cpp
|
|
9543
9549
|
msgctxt "#16300"
|
|
@@ -19635,7 +19641,13 @@ msgctxt "#35309"
|
|
|
19635
19641
|
msgid "Rewind"
|
|
19636
19642
|
msgstr ""
|
|
19637
19643
|
|
|
19638
|
-
|
|
19644
|
+
#. Label of the Manual button in the game OSD, which shows the game's instruction booklet
|
|
19645
|
+
#: addons/skin.estuary/xml/GameOSD.xml
|
|
19646
|
+
msgctxt "#35310"
|
|
19647
|
+
msgid "Manual"
|
|
19648
|
+
msgstr ""
|
|
19649
|
+
|
|
19650
|
+
#empty strings from id 35311 to 35319
|
|
19639
19651
|
|
|
19640
19652
|
#. Label of the Cheats button in the game OSD, heading of the cheats dialog, and
|
|
19641
19653
|
#. of the cheats group in settings
|
|
@@ -20002,7 +20014,55 @@ msgctxt "#35703"
|
|
|
20002
20014
|
msgid "Hardcore mode: {0:d} of {1:d} achievements unlocked"
|
|
20003
20015
|
msgstr ""
|
|
20004
20016
|
|
|
20005
|
-
#empty strings from id 35704 to
|
|
20017
|
+
#empty strings from id 35704 to 35709
|
|
20018
|
+
|
|
20019
|
+
#. Marks a locked achievement that can be missed for good
|
|
20020
|
+
#: addons/skin.estuary/xml/Includes_Games.xml
|
|
20021
|
+
msgctxt "#35710"
|
|
20022
|
+
msgid "Missable"
|
|
20023
|
+
msgstr ""
|
|
20024
|
+
|
|
20025
|
+
#. Button that picks which of a game's achievement sets is shown
|
|
20026
|
+
#: addons/skin.estuary/xml/Includes_Games.xml
|
|
20027
|
+
msgctxt "#35711"
|
|
20028
|
+
msgid "Achievement set"
|
|
20029
|
+
msgstr ""
|
|
20030
|
+
|
|
20031
|
+
#. An achievement set whose progression and win achievements are all earned
|
|
20032
|
+
#: xbmc/games/addons/cheevos/GameClientCheevos.cpp
|
|
20033
|
+
#: xbmc/games/dialogs/osd/DialogGameAchievements.cpp
|
|
20034
|
+
msgctxt "#35712"
|
|
20035
|
+
msgid "Beaten"
|
|
20036
|
+
msgstr ""
|
|
20037
|
+
|
|
20038
|
+
#. How close an achievement set is to being beaten, {0:d} earned of {1:d} needed
|
|
20039
|
+
#: xbmc/games/dialogs/osd/DialogGameAchievements.cpp
|
|
20040
|
+
#, c++-format
|
|
20041
|
+
msgctxt "#35713"
|
|
20042
|
+
msgid "{0:d} of {1:d} to beat"
|
|
20043
|
+
msgstr ""
|
|
20044
|
+
|
|
20045
|
+
#. An achievement set the player turned off
|
|
20046
|
+
#: xbmc/games/dialogs/osd/DialogGameAchievements.cpp
|
|
20047
|
+
msgctxt "#35714"
|
|
20048
|
+
msgid "Turned off"
|
|
20049
|
+
msgstr ""
|
|
20050
|
+
|
|
20051
|
+
#. The game's own achievement set, as opposed to its subsets
|
|
20052
|
+
#: xbmc/games/dialogs/osd/DialogGameAchievements.cpp
|
|
20053
|
+
msgctxt "#35715"
|
|
20054
|
+
msgid "Main Set"
|
|
20055
|
+
msgstr ""
|
|
20056
|
+
|
|
20057
|
+
#empty strings from id 35716 to 35717
|
|
20058
|
+
|
|
20059
|
+
#. Marks an achievement being attempted in the list of a game's achievements
|
|
20060
|
+
#: addons/skin.estuary/xml/Includes_Games.xml
|
|
20061
|
+
msgctxt "#35718"
|
|
20062
|
+
msgid "Attempt active"
|
|
20063
|
+
msgstr ""
|
|
20064
|
+
|
|
20065
|
+
#empty strings from id 35719 to 35999
|
|
20006
20066
|
|
|
20007
20067
|
#: xbmc/peripherals/devices/PeripheralCecAdapter.cpp
|
|
20008
20068
|
msgctxt "#36000"
|
|
@@ -20217,6 +20277,7 @@ msgstr ""
|
|
|
20217
20277
|
#: xbmc/video/VideoItemArtworkHandler.cpp
|
|
20218
20278
|
#: xbmc/video/dialogs/GUIDialogVideoInfo.cpp
|
|
20219
20279
|
#: xbmc/music/dialogs/GUIDialogMusicInfo.cpp
|
|
20280
|
+
#: xbmc/video/dialogs/GUIDialogSubtitleSettings.cpp
|
|
20220
20281
|
msgctxt "#36041"
|
|
20221
20282
|
msgid "* Item folder"
|
|
20222
20283
|
msgstr ""
|
|
@@ -7,6 +7,7 @@ import re
|
|
|
7
7
|
import time
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
|
+
from ..utils.expressions import resolve_params_in_text
|
|
10
11
|
from ..utils.files import check_paths, is_inside
|
|
11
12
|
from ..utils.infobool import condition_states
|
|
12
13
|
from ..utils.xml import get_params, get_root_from_file
|
|
@@ -30,6 +31,35 @@ def _same_definition(a, b):
|
|
|
30
31
|
return b is not None and (a.text or "").strip() == (b.text or "").strip() and dict(a.attrib) == dict(b.attrib)
|
|
31
32
|
|
|
32
33
|
|
|
34
|
+
_PARAM_REF = re.compile(r"\$PARAM\[([^,\]]+)")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _param_uses(include_node):
|
|
38
|
+
"""Yield (param, use) per $PARAM in the spliced body; forwards flag a bare $PARAM value."""
|
|
39
|
+
definition = include_node.find("definition")
|
|
40
|
+
body = definition if definition is not None else include_node
|
|
41
|
+
for node in body.iterdescendants():
|
|
42
|
+
if not isinstance(node.tag, str):
|
|
43
|
+
continue
|
|
44
|
+
parent = node.getparent()
|
|
45
|
+
target = parent.get("content", "") if parent.tag == "include" else ""
|
|
46
|
+
forward = node.get("name") if node.tag == "param" and target and "$" not in target else None
|
|
47
|
+
texts = [(None, node.text or "")]
|
|
48
|
+
texts += [(a, v) for a, v in node.attrib.items() if not a.startswith("_kdk_")]
|
|
49
|
+
for attr, value in texts:
|
|
50
|
+
if "$PARAM[" not in value:
|
|
51
|
+
continue
|
|
52
|
+
if forward and attr in (None, "value"):
|
|
53
|
+
use = ("forward", target, forward,
|
|
54
|
+
resolve_params_in_text(value)[1] == "SINGLE_UNDEFINED")
|
|
55
|
+
elif attr is None:
|
|
56
|
+
use = ("tag", node.tag)
|
|
57
|
+
else:
|
|
58
|
+
use = ("attr", attr)
|
|
59
|
+
for match in _PARAM_REF.finditer(value):
|
|
60
|
+
yield match.group(1).strip(), use
|
|
61
|
+
|
|
62
|
+
|
|
33
63
|
class Skin(Addon):
|
|
34
64
|
"""A Kodi skin: includes, colors, fonts, and resolved windows."""
|
|
35
65
|
|
|
@@ -109,9 +139,8 @@ class Skin(Addon):
|
|
|
109
139
|
return self._resource_loader
|
|
110
140
|
|
|
111
141
|
def _build_include_param_roles(self):
|
|
112
|
-
"""
|
|
142
|
+
"""Build each include body's $PARAM roles: tag, attribute, forward to nested call, multi."""
|
|
113
143
|
result = {}
|
|
114
|
-
param_re = re.compile(r"\$PARAM\[([^,\]]+)")
|
|
115
144
|
|
|
116
145
|
def mark(roles, pname, new_role):
|
|
117
146
|
existing = roles.get(pname)
|
|
@@ -122,38 +151,12 @@ class Skin(Addon):
|
|
|
122
151
|
else:
|
|
123
152
|
roles[pname] = "multi"
|
|
124
153
|
|
|
125
|
-
def role_for(node):
|
|
126
|
-
parent = node.getparent() if hasattr(node, "getparent") else None
|
|
127
|
-
if (node.tag == "param" and parent is not None
|
|
128
|
-
and parent.tag == "include" and parent.get("content")):
|
|
129
|
-
target_inc = parent.get("content")
|
|
130
|
-
target_param = node.get("name") or ""
|
|
131
|
-
if target_inc and target_param:
|
|
132
|
-
return ("forward", target_inc, target_param)
|
|
133
|
-
return None
|
|
134
|
-
|
|
135
154
|
for folder, includes_for_folder in self.include_map.items():
|
|
136
155
|
folder_result = {}
|
|
137
156
|
for inc_name, (inc_node, _defaults, _file) in includes_for_folder.items():
|
|
138
157
|
roles: dict = {}
|
|
139
|
-
for
|
|
140
|
-
|
|
141
|
-
continue
|
|
142
|
-
parent = node.getparent() if hasattr(node, "getparent") else None
|
|
143
|
-
if (node.tag == "param" and parent is inc_node):
|
|
144
|
-
continue
|
|
145
|
-
text = (node.text or "")
|
|
146
|
-
if "$PARAM[" in text:
|
|
147
|
-
forward_role = role_for(node)
|
|
148
|
-
for match in param_re.finditer(text):
|
|
149
|
-
pname = match.group(1).strip()
|
|
150
|
-
mark(roles, pname, forward_role or ("tag", node.tag))
|
|
151
|
-
for attr_name, attr_value in node.attrib.items():
|
|
152
|
-
if attr_name.startswith("_kdk_") or "$PARAM[" not in attr_value:
|
|
153
|
-
continue
|
|
154
|
-
for match in param_re.finditer(attr_value):
|
|
155
|
-
pname = match.group(1).strip()
|
|
156
|
-
mark(roles, pname, ("attr", attr_name))
|
|
158
|
+
for pname, use in _param_uses(inc_node):
|
|
159
|
+
mark(roles, pname, use[:3])
|
|
157
160
|
if roles:
|
|
158
161
|
folder_result[inc_name] = roles
|
|
159
162
|
if folder_result:
|
|
@@ -189,6 +192,52 @@ class Skin(Addon):
|
|
|
189
192
|
|
|
190
193
|
return role
|
|
191
194
|
|
|
195
|
+
def include_params(self, folder, include_name):
|
|
196
|
+
"""Each param the include declares or uses, as (name, default, used directly, forwards)."""
|
|
197
|
+
entry = self.include_map.get(folder, {}).get(include_name)
|
|
198
|
+
if entry is None:
|
|
199
|
+
return []
|
|
200
|
+
node, defaults, _file = entry
|
|
201
|
+
uses: dict[str, list] = {}
|
|
202
|
+
for param, use in _param_uses(node):
|
|
203
|
+
uses.setdefault(param, []).append(use)
|
|
204
|
+
for param in defaults:
|
|
205
|
+
uses.setdefault(param, [])
|
|
206
|
+
rows = []
|
|
207
|
+
for param, found in uses.items():
|
|
208
|
+
default = defaults.get(param)
|
|
209
|
+
forwards = []
|
|
210
|
+
for use in found:
|
|
211
|
+
if use[0] != "forward":
|
|
212
|
+
continue
|
|
213
|
+
_, target, target_param, alone = use
|
|
214
|
+
# what the target gets when a call leaves the param out; a default passes itself
|
|
215
|
+
value = None
|
|
216
|
+
if default is None and alone:
|
|
217
|
+
value = self._omitted_value(folder, target, target_param, {(include_name, param)})
|
|
218
|
+
if (target, target_param, value) not in forwards:
|
|
219
|
+
forwards.append((target, target_param, value))
|
|
220
|
+
direct = any(use[0] != "forward" for use in found)
|
|
221
|
+
rows.append((param, default, direct, forwards))
|
|
222
|
+
return rows
|
|
223
|
+
|
|
224
|
+
def _omitted_value(self, folder, include_name, param, seen):
|
|
225
|
+
"""Value a param takes when a call omits it; None unless one default reaches every use."""
|
|
226
|
+
entry = self.include_map.get(folder, {}).get(include_name)
|
|
227
|
+
if entry is None or (include_name, param) in seen:
|
|
228
|
+
return None
|
|
229
|
+
node, defaults, _file = entry
|
|
230
|
+
if param in defaults:
|
|
231
|
+
return defaults[param]
|
|
232
|
+
values = set()
|
|
233
|
+
for found, use in _param_uses(node):
|
|
234
|
+
if found != param:
|
|
235
|
+
continue
|
|
236
|
+
if use[0] != "forward" or not use[3]:
|
|
237
|
+
return None
|
|
238
|
+
values.add(self._omitted_value(folder, use[1], use[2], seen | {(include_name, param)}))
|
|
239
|
+
return values.pop() if len(values) == 1 else None
|
|
240
|
+
|
|
192
241
|
def _load_colors_and_fonts(self):
|
|
193
242
|
"""Load colors and fonts through the resource reader, dropping the validation index."""
|
|
194
243
|
from ..coredata import kodi_colors_xml
|
|
@@ -11,7 +11,7 @@ _VALUE_KEY = re.compile(r'(?:%22|")value(?:%22|")\s*(?:%3A|:)', re.IGNORECASE)
|
|
|
11
11
|
_VALUE_END = re.compile(r'%5D|%7D|[\]}]', re.IGNORECASE)
|
|
12
12
|
_EXPRESSION = re.compile(r'\$(ESC)?(INFO|VAR)\[')
|
|
13
13
|
_PLUGIN_QUERY = re.compile(r'plugin://[^\s"\'?]*\?', re.IGNORECASE)
|
|
14
|
-
_URL_END = re.compile(r'[
|
|
14
|
+
_URL_END = re.compile(r'["\',)]')
|
|
15
15
|
# an '&' that starts no key=value pair, or a '|', inside a value
|
|
16
16
|
_DATA_SEPARATOR = re.compile(r'&(?![\w.-]+=)|\|')
|
|
17
17
|
# the key of the last pair a text opens, as in ?key= or &key=
|
|
@@ -23,6 +23,10 @@ _ENCODER = re.compile(r'get_encoded_string\b.*?\bwindow_prop=([^&"\s,)]+)', re.I
|
|
|
23
23
|
_PROPERTY = re.compile(r'window(?:\([^)]*\))?\.property\((.+)\)$', re.IGNORECASE)
|
|
24
24
|
_SKIN_STRING = re.compile(r'skin\.string\((.+)\)$', re.IGNORECASE)
|
|
25
25
|
_ITEM_PROPERTY = re.compile(r'container\((\d+)\)\.listitem\w*(?:\([^)]*\))?\.property\((.+)\)$', re.IGNORECASE)
|
|
26
|
+
_ITEM_LABEL = re.compile(r'container\((\d+)\)\.listitem\w*(?:\([^)]*\))?\.label$', re.IGNORECASE)
|
|
27
|
+
# script.skinvariables fills a list with one item per && path, its label URL-encoded
|
|
28
|
+
_ENCODER_LIST = re.compile(r'\binfo=get_encoded_string\b', re.IGNORECASE)
|
|
29
|
+
_ONLY_VAR = re.compile(r'\s*\$VAR\[([^\]]+)\]\s*$')
|
|
26
30
|
# an edit control's text as typed; no other control answers Index(1)
|
|
27
31
|
_TYPED = re.compile(r'control\.getlabel\(\s*\d+\s*\)\.index\(\s*1\s*\)$', re.IGNORECASE)
|
|
28
32
|
# characters a URL query or a JSON string cannot carry unencoded
|
|
@@ -47,7 +51,7 @@ def _plain(name):
|
|
|
47
51
|
|
|
48
52
|
|
|
49
53
|
def _url_end(text, pos):
|
|
50
|
-
"""Index where the URL starting before the position ends: a
|
|
54
|
+
"""Index where the URL starting before the position ends: a quote, comma or ')'."""
|
|
51
55
|
found = _URL_END.search(text, pos)
|
|
52
56
|
return found.start() if found else len(text)
|
|
53
57
|
|
|
@@ -58,7 +62,7 @@ def _worst(kinds):
|
|
|
58
62
|
|
|
59
63
|
|
|
60
64
|
def value_sources(paths):
|
|
61
|
-
"""Each value the skin stores per property, skin string
|
|
65
|
+
"""Each value the skin stores per property, skin string, static item property and list id."""
|
|
62
66
|
sources = {}
|
|
63
67
|
for path in paths:
|
|
64
68
|
root = get_root_from_file(path)
|
|
@@ -72,6 +76,11 @@ def value_sources(paths):
|
|
|
72
76
|
if control is not None and (control.get("id") or "").isdigit():
|
|
73
77
|
key = f"item:{control.get('id')}:{element.get('name').casefold()}"
|
|
74
78
|
sources.setdefault(key, []).append(element.text or "")
|
|
79
|
+
parent = element.getparent()
|
|
80
|
+
if element.tag == "content" and parent is not None and parent.tag == "control":
|
|
81
|
+
control_id = parent.get("id") or ""
|
|
82
|
+
if control_id.isdigit():
|
|
83
|
+
sources.setdefault(f"content:{control_id}", []).append((element.text or "").strip())
|
|
75
84
|
for text in [element.text or ""] + list(element.attrib.values()):
|
|
76
85
|
for name, args in builtin_calls(text, ("SetProperty", "Skin.SetString")):
|
|
77
86
|
if not args or "$" in args[0]:
|
|
@@ -171,8 +180,25 @@ class UrlValues:
|
|
|
171
180
|
return SAFE if _plain(found.group(1)) else UNTRACED
|
|
172
181
|
if _TYPED.search(info):
|
|
173
182
|
return TYPED
|
|
183
|
+
found = _ITEM_LABEL.search(info)
|
|
184
|
+
if found and self._encoder_list(found.group(1)):
|
|
185
|
+
return SAFE
|
|
174
186
|
return SAFE if _plain(info.rsplit(".", 1)[-1]) else RAW
|
|
175
187
|
|
|
188
|
+
def _encoder_list(self, control_id):
|
|
189
|
+
"""True when lists with the id exist and the encoder fills each one."""
|
|
190
|
+
paths = self.sources.get(f"content:{control_id}")
|
|
191
|
+
return bool(paths) and all(self._encoder_path(path, frozenset()) for path in paths)
|
|
192
|
+
|
|
193
|
+
def _encoder_path(self, text, seen):
|
|
194
|
+
"""True when a list path calls the encoder, or is only a $VAR whose every value does."""
|
|
195
|
+
if _ENCODER_LIST.search(text):
|
|
196
|
+
return True
|
|
197
|
+
found = _ONLY_VAR.match(text)
|
|
198
|
+
name = found.group(1).strip() if found else ""
|
|
199
|
+
values = self.variables.get(name) if name not in seen else None
|
|
200
|
+
return bool(values) and all(self._encoder_path(value, seen | {name}) for value in values)
|
|
201
|
+
|
|
176
202
|
def _stored_kind(self, key, seen):
|
|
177
203
|
"""Kind of a property, skin string or static item property, from every value it holds."""
|
|
178
204
|
if key in seen:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kdk
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.3.1
|
|
4
4
|
Summary: Kodi skin validation tool - CLI and GUI
|
|
5
5
|
Author-email: MikeSiLVO <mikesilvo164@gmail.com>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -54,7 +54,7 @@ contain the same code; the GUI build runs without a console window, so
|
|
|
54
54
|
double-clicking it doesn't open a terminal. No Python install needed.
|
|
55
55
|
|
|
56
56
|
Download them from the [Releases page](https://github.com/MikeSiLVO/kdk/releases).
|
|
57
|
-
`<version>` is the release tag, such as `v3.
|
|
57
|
+
`<version>` is the release tag, such as `v3.3.1`:
|
|
58
58
|
|
|
59
59
|
- **Windows (x86_64)**: `kdk-gui-<version>-windows-x86_64.exe` and
|
|
60
60
|
`kdk-cli-<version>-windows-x86_64.exe`.
|
|
@@ -196,7 +196,7 @@ still points at the right file.
|
|
|
196
196
|
Run it on `ubuntu-latest`. Windows runners check text files out with CRLF line
|
|
197
197
|
endings, which File Integrity reports as errors.
|
|
198
198
|
|
|
199
|
-
##
|
|
199
|
+
## Muting a finding
|
|
200
200
|
|
|
201
201
|
An XML comment mutes warnings the skin has already judged, the way a
|
|
202
202
|
`# type: ignore` does in Python. `kdk-ignore-file` covers the whole file it sits
|
|
@@ -16,11 +16,35 @@ PATH = "videodb://movies/titles/?xsp=%7B%22rules%22%3A%7B%22and%22%3A%5B%7B%22fi
|
|
|
16
16
|
"%22operator%22%3A%22is%22%2C%22value%22%3A{value}%7D%5D%7D%2C%22type%22%3A%22movies%22%7D"
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
ENCODER = ("plugin://script.skinvariables/?info=get_encoded_string&unencoded_paths=true"
|
|
20
|
+
"&&$INFO[ListItem.Label]&&$INFO[ListItem.Title]")
|
|
21
|
+
|
|
22
|
+
|
|
19
23
|
def path(value, field="title"):
|
|
20
24
|
"""An xsp path with a single "is" rule, percent-encoded as skins write it."""
|
|
21
25
|
return PATH.format(field=field, value=value)
|
|
22
26
|
|
|
23
27
|
|
|
28
|
+
def url_rows(home, includes="<includes/>"):
|
|
29
|
+
"""The xsp and plugin URL rows a one-window skin with that Home.xml and Includes.xml gives."""
|
|
30
|
+
files = {
|
|
31
|
+
"addon.xml": '<addon id="skin.t" version="1.0.0"><requires><import addon="xbmc.gui" '
|
|
32
|
+
'version="5.17.0"/></requires><extension point="xbmc.gui.skin">'
|
|
33
|
+
'<res folder="16x9" default="true"/></extension></addon>',
|
|
34
|
+
"16x9/Includes.xml": includes,
|
|
35
|
+
"16x9/Home.xml": home,
|
|
36
|
+
}
|
|
37
|
+
with tempfile.TemporaryDirectory() as skin:
|
|
38
|
+
for rel, text in files.items():
|
|
39
|
+
os.makedirs(os.path.dirname(os.path.join(skin, rel)), exist_ok=True)
|
|
40
|
+
with open(os.path.join(skin, rel), "w", encoding="utf-8") as f:
|
|
41
|
+
f.write(text)
|
|
42
|
+
provider = InfoProvider()
|
|
43
|
+
provider.settings = {}
|
|
44
|
+
provider.init_addon(skin)
|
|
45
|
+
return [r for r in provider.check_values() if r.get("rule", "").startswith(("xml.xsp", "xml.plugin"))]
|
|
46
|
+
|
|
47
|
+
|
|
24
48
|
class TestUrlValues(unittest.TestCase):
|
|
25
49
|
"""Sorting of each runtime value in a rule's value list into raw, untraced or safe."""
|
|
26
50
|
|
|
@@ -78,7 +102,17 @@ class TestUrlValues(unittest.TestCase):
|
|
|
78
102
|
f.write('<window><controls><control type="list" id="9110"><content>'
|
|
79
103
|
'<item><label>a</label><property name="Dir">$INFO[ListItem.Director]</property></item>'
|
|
80
104
|
'</content></control></controls></window>')
|
|
81
|
-
self.assertEqual(value_sources([path]), {"item:9110:dir": ["$INFO[ListItem.Director]"]})
|
|
105
|
+
self.assertEqual(value_sources([path]), {"item:9110:dir": ["$INFO[ListItem.Director]"], "content:9110": [""]})
|
|
106
|
+
|
|
107
|
+
def test_scan_reads_each_list_path_by_id(self):
|
|
108
|
+
with tempfile.TemporaryDirectory() as folder:
|
|
109
|
+
path = os.path.join(folder, "Home.xml")
|
|
110
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
111
|
+
f.write('<window><controls><control type="list" id="4001"><content>'
|
|
112
|
+
f'{ENCODER.replace("&", "&")}</content></control>'
|
|
113
|
+
'<control type="list"><content>videodb://movies/titles/</content></control>'
|
|
114
|
+
'</controls></window>')
|
|
115
|
+
self.assertEqual(value_sources([path]), {"content:4001": [ENCODER]})
|
|
82
116
|
|
|
83
117
|
def test_keyboard_skin_string_is_raw(self):
|
|
84
118
|
self.assertEqual(self.kinds("%5B%22$INFO[Skin.String(search)]%22%5D", {"skin:search": ["keyboard"]}),
|
|
@@ -102,7 +136,7 @@ class TestUrlRows(unittest.TestCase):
|
|
|
102
136
|
"""Xsp and plugin URL rows: errors for raw or escaped typed values, warnings for the rest."""
|
|
103
137
|
|
|
104
138
|
def test_rows(self):
|
|
105
|
-
"""
|
|
139
|
+
"""Test the rule, severity and subject of every URL fault in one window."""
|
|
106
140
|
home = (
|
|
107
141
|
'<window><controls>'
|
|
108
142
|
'<control type="button" id="10">'
|
|
@@ -125,22 +159,7 @@ class TestUrlRows(unittest.TestCase):
|
|
|
125
159
|
'<control type="list" id="56"><content>' + path("%5B%22$INFO[Control.GetLabel(9).Index(1)]%22%5D").replace("&", "&")
|
|
126
160
|
+ '</content></control>'
|
|
127
161
|
'</controls></window>')
|
|
128
|
-
|
|
129
|
-
"addon.xml": '<addon id="skin.t" version="1.0.0"><requires><import addon="xbmc.gui" '
|
|
130
|
-
'version="5.17.0"/></requires><extension point="xbmc.gui.skin">'
|
|
131
|
-
'<res folder="16x9" default="true"/></extension></addon>',
|
|
132
|
-
"16x9/Includes.xml": "<includes/>",
|
|
133
|
-
"16x9/Home.xml": home,
|
|
134
|
-
}
|
|
135
|
-
with tempfile.TemporaryDirectory() as skin:
|
|
136
|
-
for rel, text in files.items():
|
|
137
|
-
os.makedirs(os.path.dirname(os.path.join(skin, rel)), exist_ok=True)
|
|
138
|
-
with open(os.path.join(skin, rel), "w", encoding="utf-8") as f:
|
|
139
|
-
f.write(text)
|
|
140
|
-
provider = InfoProvider()
|
|
141
|
-
provider.settings = {}
|
|
142
|
-
provider.init_addon(skin)
|
|
143
|
-
rows = [r for r in provider.check_values() if r.get("rule", "").startswith(("xml.xsp", "xml.plugin"))]
|
|
162
|
+
rows = url_rows(home)
|
|
144
163
|
found = sorted((r["rule"], r["severity"], r["subject"]) for r in rows)
|
|
145
164
|
self.assertEqual(found, [
|
|
146
165
|
("xml.plugin-typed-text", "warning", "$INFO[Control.GetLabel(312).Index(1)]"),
|
|
@@ -156,6 +175,29 @@ class TestUrlRows(unittest.TestCase):
|
|
|
156
175
|
self.assertIn("'Law & Order' empties the list", error)
|
|
157
176
|
self.assertIn("URL-encode it first", error)
|
|
158
177
|
|
|
178
|
+
def test_encoder_list_labels_in_a_plugin_url(self):
|
|
179
|
+
"""The labels of an encoder list pass through variables unreported."""
|
|
180
|
+
includes = (
|
|
181
|
+
'<includes>'
|
|
182
|
+
'<variable name="Path_Encoded_Label"><value>$INFO[Container(4001).ListItemAbsolute(0).Label]</value></variable>'
|
|
183
|
+
'<variable name="Path_Raw_Label"><value>$INFO[Container(4002).ListItemAbsolute(0).Label]</value></variable>'
|
|
184
|
+
'<variable name="Path_Youtube_Query"><value>$VAR[Path_Encoded_Label]</value></variable>'
|
|
185
|
+
'</includes>')
|
|
186
|
+
home = (
|
|
187
|
+
'<window><controls>'
|
|
188
|
+
'<control type="list" id="4001"><content>' + ENCODER.replace("&", "&") + '</content></control>'
|
|
189
|
+
'<control type="list" id="4002"><content>videodb://tvshows/titles/</content></control>'
|
|
190
|
+
'<control type="button" id="10">'
|
|
191
|
+
'<onclick>ActivateWindow(Videos,plugin://plugin.video.youtube/kodion/search/query/'
|
|
192
|
+
'?q=$VAR[Path_Youtube_Query],return)</onclick>'
|
|
193
|
+
'<onclick>ActivateWindow(Videos,plugin://plugin.video.youtube/kodion/search/query/'
|
|
194
|
+
'?q=$VAR[Path_Raw_Label],return)</onclick>'
|
|
195
|
+
'</control>'
|
|
196
|
+
'</controls></window>')
|
|
197
|
+
rows = url_rows(home, includes)
|
|
198
|
+
self.assertEqual(sorted((r["rule"], r["subject"]) for r in rows),
|
|
199
|
+
[("xml.plugin-unencoded", "$INFO[Container(4002).ListItemAbsolute(0).Label]")])
|
|
200
|
+
|
|
159
201
|
|
|
160
202
|
class TestPluginValues(unittest.TestCase):
|
|
161
203
|
"""Runtime values in a plugin URL's query, which the add-on splits on & and decodes."""
|
|
@@ -180,6 +222,10 @@ class TestPluginValues(unittest.TestCase):
|
|
|
180
222
|
text = 'RunPlugin("plugin://script.skinvariables/?info=get_encoded_string&window_prop=E&&$INFO[ListItem.Label]")'
|
|
181
223
|
self.assertEqual(self.kinds(text), [])
|
|
182
224
|
|
|
225
|
+
def test_a_space_stays_inside_the_query(self):
|
|
226
|
+
self.assertEqual(self.kinds("plugin://p/?q=$INFO[ListItem.Artist] $INFO[ListItem.Album] Album"),
|
|
227
|
+
[("$INFO[ListItem.Artist]", RAW, None), ("$INFO[ListItem.Album]", RAW, None)])
|
|
228
|
+
|
|
183
229
|
def test_only_the_query_counts(self):
|
|
184
230
|
self.assertEqual(self.kinds("plugin://x/$INFO[ListItem.Title]/"), [])
|
|
185
231
|
self.assertEqual(self.kinds("RunPlugin(plugin://x/?a=b) + $INFO[ListItem.Title]"), [])
|
|
@@ -199,6 +245,26 @@ class TestPluginValues(unittest.TestCase):
|
|
|
199
245
|
self.assertEqual(self.kinds("plugin://p/?genre=$VAR[Genre]", variables=variables),
|
|
200
246
|
[("$VAR[Genre]", RAW, None)])
|
|
201
247
|
|
|
248
|
+
def test_labels_of_an_encoder_list_are_safe(self):
|
|
249
|
+
"""An encoder list holds an encoded label per item."""
|
|
250
|
+
sources = {"content:4001": [ENCODER]}
|
|
251
|
+
variables = {"Path_Encoded_Label": ["$INFO[Container(4001).ListItemAbsolute(0).Label]"],
|
|
252
|
+
"Path_Youtube_Query": ["$VAR[Path_Encoded_Label]"], "Encoder": [ENCODER]}
|
|
253
|
+
self.assertEqual(self.kinds("plugin://p/?q=$VAR[Path_Youtube_Query]", sources, variables), [])
|
|
254
|
+
self.assertEqual(self.kinds("plugin://p/?q=$INFO[Container(4001).ListItem(1).Label]", sources), [])
|
|
255
|
+
self.assertEqual(self.kinds("plugin://p/?q=$INFO[Container(4001).ListItem.Label]",
|
|
256
|
+
{"content:4001": ["$VAR[Encoder]"]}, variables), [])
|
|
257
|
+
|
|
258
|
+
def test_labels_of_other_lists_stay_raw(self):
|
|
259
|
+
label = "$INFO[Container(4001).ListItemAbsolute(0).Label]"
|
|
260
|
+
text = f"plugin://p/?q={label}"
|
|
261
|
+
self.assertEqual(self.kinds(text), [(label, RAW, None)])
|
|
262
|
+
self.assertEqual(self.kinds(text, {"content:4001": ["videodb://movies/titles/"]}), [(label, RAW, None)])
|
|
263
|
+
self.assertEqual(self.kinds(text, {"content:4001": [ENCODER, ""]}), [(label, RAW, None)])
|
|
264
|
+
self.assertEqual(self.kinds(text, {"content:4001": ["$VAR[Missing]"]}), [(label, RAW, None)])
|
|
265
|
+
self.assertEqual(self.kinds("plugin://p/?q=$INFO[Container(4001).ListItem.Title]", {"content:4001": [ENCODER]}),
|
|
266
|
+
[("$INFO[Container(4001).ListItem.Title]", RAW, None)])
|
|
267
|
+
|
|
202
268
|
def test_time_date_and_sort_words_need_no_encoding(self):
|
|
203
269
|
self.assertEqual(self.kinds("plugin://p/?t=$INFO[System.Time(hh)]&d=$INFO[System.Date]"
|
|
204
270
|
"&s=$INFO[Container.SortMethod]&o=$INFO[Container.SortOrder]"), [])
|
{kdk-3.2.0 → kdk-3.3.1}/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|