Opravný deploy 2.1.16 / skin 1.3.10 zveřejněn 2026-10-04. ZIP 1 842 286 B; oba veřejné soubory ověřeny přes HTTP a SHA256. Statické textury používají přímé cesty; instalátor připraví nativní texture bundle před aktivací skinu. Deploy postup opraven. GUI nebylo testováno.

# Milionář widget, refresh and installation audit

Date: 2026-10-04. Status: 2.1.15 released; texture regression correction in progress.
The findings and baseline below describe the code at the initial audit.

## 2.1.16 texture regression correction

The user reported missing Home icons, button backgrounds and focus highlights
on a fresh device. The screenshot shows the image-based sidebar icons and
selection strip rendered, while the radio/button-based icons and backgrounds
are absent. This is consistent with the confirmed API distinction in Kodi
21.3: `GUIControlFactory::GetTexture` stores a literal filename, while
`GetInfoTexture` evaluates a `CGUIInfoLabel`. The 2.1.15 packager incorrectly
prepended a runtime Home `$INFO` property to both kinds of texture.

The earlier syntax/manifest/asset-existence checks did not validate this control
contract. Their passing results did not prove GUI rendering correctness.

Correction: release XML uses literal Estuary texture names. Before a skin swap,
the installer copies the device's existing native `Textures.xbt` into the staged
skin's `media` directory. No archive or extra image is bundled in the ZIP.
Kodi's normal texture manager resolves the literal names, including textures
passed through includes to radio controls and button focus textures. Copy or
archive-validation failures preserve the installed skin. Runtime Home texture
properties are no longer needed. Cache and watched-state work is retained.

Regression checks cover the packaged Home includes, native archive installation,
failure recovery and the literal texture contract. No GUI/device testing was
authorized or performed; this remains an explicit validation limit.

Release: addon 2.1.15 / skin 1.3.9, 356 files, 1,847,354 bytes (90.8% smaller
than the deployed 2.1.14 ZIP). One logo; no other bundled images/fonts or dev
files. ZIP integrity, staged syntax/XML/import/asset declarations, migration
translations, secret scan, version guard and public version/latest download
hashes passed. Published through `/deploy`; no agent/development ZIP uploaded.
SHA256: `cb071655953dfc613f5edcb3cc8533dabe9ea1b1696f4620fe517bdec98cb2c8`.

## Implemented result

- Persistent per-row revisions are committed with each actual state change.
  Only slots using affected row types receive a new content URL. Notification
  bursts are coalesced; a revision poll repairs a lost notification.
- Local rows use atomic JSON snapshots scoped to profile, database, language,
  date and configuration. Bulk reads and early limits avoid constructing items
  outside the selected page. A 100-movie/limit-10 probe now builds ten ListItems,
  with five cold DB reads or one warm revision read, instead of 100/102.
- Legacy MyTV, MyCinema, games, AI, TMDb/Kraska and Vinyl retain cached content
  during background refresh. Valid empty results replace old content; failures
  preserve the last snapshot and retry with bounded work and backoff. MyTV
  next episodes cross seasons using cached, bulk-loaded metadata.
- Watched state commits immediately in one transaction. A cached 400-episode
  series probe uses one transaction and no foreground RPC or provider calls.
  Missing episode coverage and Kodi mirroring use durable, ordered jobs;
  version guards prevent delayed results from overwriting newer local state.
- Metadata publication is atomic across a show and its seasons. Unchanged
  payloads renew TTL without refreshing widgets. Failed titles rotate within
  a bounded background budget instead of starving later titles.
- Migration is an explicit plugin-thread Yes/No summary followed by modal
  progress. Automatic discovery first checks whether any new list is populated,
  skips empty old lists, and respects declined/completed fingerprints. Manual
  retry resumes safe merge, metadata and Home preparation. Completion requires
  published revisions and visible widget evidence; timeout remains pending.
  Originals and fsynced backup copies are retained.
- Skin installation validates before swapping, reports copy progress and
  recovers interrupted swaps. The automatic migration offer waits only for
  disk installation and quiet UI, rather than a later Home visit.
- The optional weak-device profile caps Home rows at ten items, reduces known
  artwork sizes and suppresses costly Home effects. Normal mode also benefits
  from the cache, bounded reads, revision routing and watched-state changes.
- The release contains one Milionář logo. Shared Estuary textures/fonts replace
  custom bundled artwork/fonts; development modules, fixture routes, generators,
  screenshots and documentation are excluded. Czech, Slovak and English are
  retained, with English fallback for other languages. Legal notices remain included.
  Native substitutes preserve navigation but are not pixel-identical artwork.

240 focused offline tests passed, covering state races/restart, empty/error refresh, snapshot
scope, bounded operation counts, installation recovery, schema upgrade and
migration completion/retry. No GUI/device/provider tests were performed.
Old next-up fixture tests remain unavailable because their pre-existing
`fixtures/tmdb_fixture.json` is missing. Device timings and appearance require
separately authorized validation on the target Android TV.

## Scope and evidence

Reviewed all 38 configurable Home row types, the six search rows, inherited
Kodi Home content, widget entry points, local state mutations, network cache
workers, watch-state mirroring, skin installation and release packaging.
The inherited sections were reviewed for their source/refresh contracts;
their individual rendering and provider performance were not measured.

User reports: even one watched item is slow; whole-series marking takes
minutes; first start and reload are slow; MyTV can remain stale; reinstall
through Kodi's **Install from ZIP** has a long period without useful progress.
The requested outcome is a
proposal first, with cached content retained during refresh.

Initial evidence consisted of code inspection, narrowly scoped synthetic offline
probes using the existing Kodi stubs, a sanitized read of the current Kodi
log, and upstream Kodi **21.3-Omega** source. No GUI/device/browser testing,
installation, release build, deployment, live provider calls or credential
reads were performed. Existing unrelated working-tree changes were preserved.

The current log contained 560 lines, four network-widget cache publications
and one background library refresh. It contained no widget timing samples
or matches for the checked widget/refresh/database-lock/tracker errors.
It cannot establish Android timings or installation duration. The installed
skin on this Mac is a development symlink, so it skips production copying.

## Confirmed offline baselines

| Probe | Result | Meaning |
|---|---|---|
| 100 synthetic listed movies, row limit 10 | 10 returned, **100 ListItems created, 102 DB connections** | Limit is applied after construction; metadata is read once per movie. |
| Mark a fully cached synthetic series with 120 aired episodes | **483 DB connections, 120 Files.SetFileDetails calls** | Database and Kodi synchronization scale per episode on the interactive path. |
| Recommendation builder successfully returns an empty list | Refresh returns false; old cached item remains | Legitimate empty content is not published. |
| Named recommendation invalidation | Cache deleted; **zero service notifications** | Invalidation does not request rebuilding. |
| Read that now-missing recommendation cache | Empty widget; **zero service notifications** | Cache misses do not wake the service. |

These are operation counts, not device speed measurements. RPC was stubbed;
there were no real Kodi writes. Temporary probe databases were removed.
An initial cache probe hit the shell Python's missing `requests` dependency;
it was rerun successfully with a synthetic provider module. No dependency
was installed and no request was sent.

## Findings, ordered by impact

### F1 — Global refresh amplification (critical)

`wcs/skin/widgets.py:399`, `wcs/skin/refresher.py:45`,
`scripts/skin_harness/gen_slots.py:19`, `service.py:264`.

All 180 generated content-path variants use `milionar.widgets.token`.
There are 60 available slots and three mutually exclusive style variants
per slot: **180 paths does not mean 180 running widgets**.
Local mutation, every published network cache, Vinyl refresh, library
refresh, player stop and library notifications can change this global token.
The tracker also invalidates after resume saves every ten seconds.
Separate completions therefore produce multiple refresh waves, including
rows whose data has not changed. Named invalidation only selects which
cache files to delete; it does not target row refreshes.

Recommendation: persistent content revisions per dependency and per actual
row result; one publication for a burst of changes; no revision change for
identical content. Update only affected slot URLs, including duplicate slots
in other sections when they share changed data. A section is not a sufficient
dependency key: MyTV can also appear under Continue.

### F2 — Interactive watched action does bulk I/O and network discovery (critical)

`wcs/library2/ctx.py:25`, `:34`, `:50`; `wcs/library2/mirror.py:27`;
`wcs/library2/store.py:68`, `:288`.

Each target is read, written in its own transaction, reread for mirroring,
written through JSON-RPC, then written again to record mirror state. Only
after the entire loop does `_done()` publish refresh and Container.Refresh.
For a missing series/season cache, `_episodes()` fetches metadata inline.
Individual-item toggles also build their eligibility from broad state reads.
The DB busy timeout is five seconds; lock waits can compound these costs.

Recommendation: one local transaction for the complete known target set,
idempotent manual marks, durable pending mirror work, immediate affected-row
publication, then bounded asynchronous mirroring. Preserve playcount,
resume-clearing, aired-episode and specials semantics. If episode coverage
is missing, prepare it in the background with explicit progress; never
silently claim an entire series is marked when only cached seasons were used.

### F3 — Widget output is repeatedly rebuilt despite existing data caches (high)

`wcs/library2/rows.py:63`, `:306`, `:334`, `:452`;
`wcs/skin/widgets.py:374`; `wcs/vinyl/widgets.py:117`.

SQLite metadata and network JSON caches already exist. However, library
rows rebuild cards and ListItems on every plugin execution. Movies are not
preloaded in bulk. All candidates become ListItems before applying the
requested limit. Series rows bulk-load metadata but repeatedly reconstruct
episode order and counts across widget invocations. Genre and local search
branches return before the common limit. Legacy builders dispatched by
`rows.run()` receive only `action` and `click`, dropping limit/sort/style.

Recommendation: first bulk-read the needed data, filter/sort plain cards,
apply limits, then construct the selected ListItems. Cache plain row output
across plugin invocations using the content revision and configuration.
Share series summaries where needed, rather than duplicating whole season
payloads into every row. Use `offscreen=True` for plugin listing items;
do not change manually managed WindowXML dialog items indiscriminately.

### F4 — Missing refresh requests and stale empty results (high)

`wcs/skin/widgets.py:293`, `:399`; `wcs/skin/refresher.py:116`;
`wcs/ai/MediaChannelManager.py:75`.

Both TV and cinema edits delete `widget_mycinema_recommended` and refresh
all rows. Neither requests an immediate cache rebuild. Missing widget
caches do not send the documented refresh notification. Scheduled checks
normally run every 15 minutes, so a removed cache can remain empty until
a later successful check. A legitimate empty result never replaces old
content. Provider list helpers can collapse request failure into `[]`,
so success-empty and failure cannot simply be treated as equivalent.

Recommendation: keep the last valid snapshot, mark only the affected source
dirty, queue refresh immediately, and publish successful empty results.
Keep failures separate from successful empty content; retain the old snapshot
on failure and retry with backoff. A cache miss should request deduplicated
work, not wait for the schedule.

### F5 — Mutation coverage is inconsistent (high)

`wcs/ai/ChannelHistory.py:65`, `:83`, `:154`, `:202`;
`wcs/user_data.py:156`, `:174`, `:199`, `:487`;
`wcs/ai/RecommendationHistory.py:24`.

Channel history/progress writes have no widget publication hook. Shared
legacy movie/series/history writers also do not publish a targeted change.
Several callers refresh only their current container or depend on playback
events. AI recommendation publication is implemented in one dialog caller,
not in the writer. These contracts leave correctness dependent on the path
used to modify data.

Recommendation: publish changes at successful persistence boundaries,
with media/entity/channel/source identity. Cover every entry path, including
dialogs, context menus, playback, import/reset and settings. Avoid duplicated
caller publications. MyTV progress, library episode watch state and history
are distinct stores: map their dependencies explicitly before any merge.

### F6 — Background work is oversized and insufficiently recoverable (high)

`wcs/vinyl/client.py:106`, `:125`; `wcs/vinyl/widgets.py:33`;
`wcs/skin/refresher.py:91`, `:149`; `service.py:257`;
`wcs/library2/refresher.py:26`, `:36`; `wcs/library2/tracker.py:211`.

Vinyl refresh fetches all account sources and all song pages even when only
one source changed or only curator/discover widgets are used. It precedes
the other network widget jobs on the same worker. MyCinema recommendations
iterate every source movie, including duplicates across cinemas. Cache
publication happens even when display data is unchanged. Manual refresh
sets a completed property/notification without aggregating failures.

Library refresh checks show freshness, not individual season coverage.
It writes the show before its seasons; partial failure can leave seasons
missing while the show is considered fresh. Its limit counts successful
shows, so failures can cause attempts over the entire due list. The tracker
removes pending mirror work before checking whether RPC succeeded. Pending
work is in memory and has no durable retry contract. Settings changes wake
the worker without forcing specific still-fresh affected provider caches.

Recommendation: bounded work queue, visible-row priority, deduplication,
per-source jobs, retry/backoff, cancellation on shutdown and source change,
success/partial/failure state, and durable mirror retries. Fetch only what
active rows need, with paging retained for user-requested complete lists.
Check metadata coverage per season and publish a coherent cache generation.

### F7 — MyTV next-episode widget has correctness and repeated-read issues (high)

`wcs/skin/widgets.py:137`; `wcs/ai/ChannelHistory.py:134`.

The builder reads a channel history file again for each series and displays
`current episode + 1` in the same season. It does not handle season rollover
or episode availability from the cached series episode order. Progress
mutations do not directly invalidate the row.

Recommendation: read each channel history once per snapshot, use cached
episode ordering for rollover/air dates while preserving channel-specific
progress, and refresh this row on channel-progress changes.

### F8 — Avoidable UI bookkeeping and full skin reloads (medium)

`wcs/library2/tracker.py:415`, `:441`; `wcs/skin/context.py:118`, `:189`;
`wcs/skin/slots.py:158`, `:367`; `wcs/library2/listings.py:203`, `:318`.

The tracker polls focus twice per second and scans every current addon
container item for Kodi mirror changes every five seconds. Each eligible
item can generate a Files.GetFileDetails RPC, whose core implementation
also resolves directory information. Legacy plugin listings construct
their context model per title. Listing paths sometimes reread metadata
already loaded for cards. Widget-manager commit writes every slot field
then calls ReloadSkin, even for changes that only affect a content URL.

Recommendation: event-triggered adoption with bounded fallback scans;
retain focus polling only where needed for the menu contract; reuse one
context/data model per listing and avoid building unused menus. Diff widget
configuration before writing. Keep ReloadSkin only for layout/include
changes that actually require reparsing.

### F9 — Installation payload and skin copy add noticeable I/O (high)

`scripts/build_zip.sh`; `wcs/skin/installer.py:56`, `:92`, `:171`.

Estimated package candidates under current packaging exclusions:
**1,292 files, 33.64 MiB uncompressed**. This is not a built/downloaded ZIP.
Bundled skin contributes **949 files, 19.88 MiB**, including five screenshots
totaling **7.89 MiB**. The current build removes screenshots from the root
addon manifest but retains the skin screenshots and their manifest entries.
On production devices a skin version change copies the complete skin again,
renames directories, deletes the old tree, registers addons and reloads
the active skin. Copying and cleanup have no user progress. The ordinary
same-skin-version path already skips that copy.

Recommendation: exclude skin screenshots from runtime ZIPs and remove their
packaged manifest entries together; preserve runtime assets, fonts and
licences. Reduce oversized artwork after actual display/reference checks.
Investigate Kodi texture packing for loose skin media only if profiling
justifies it. Show native background progress for the bundled-skin phase;
prepare before swap, validate staged assets, recover interrupted swaps and
defer nonessential cleanup. Do not replace the native addon updater with a
self-modifying installer. A separate skin package/repository is an optional
distribution decision, not a prerequisite for widget fixes.

## Kodi capabilities and limits checked against upstream

1. **Disk directory cache is conditional.** Python `cacheToDisc=True`
   selects `CACHE_IF_SLOW`, not an unconditional persistent widget cache.
   The media-window code loads/saves disk listings and saves slow listings
   after one second. Home's DirectoryProvider follows a different path.
   Merely toggling this flag does not guarantee a cached cold Home startup.
   Use native caching for compatible listings with correct invalidation,
   while keeping persistent plain widget snapshots as the reliable fallback.
   [PluginDirectory](https://github.com/xbmc/xbmc/blob/21.3-Omega/xbmc/filesystem/PluginDirectory.cpp),
   [GUIMediaWindow](https://github.com/xbmc/xbmc/blob/21.3-Omega/xbmc/windows/GUIMediaWindow.cpp).
2. **Core also requests broad refreshes.** The Home provider reacts to
   player/library announcements and changed URLs; in-flight jobs can be
   canceled by another refresh. It keeps old results until successful
   completion and the container attempts to preserve selection by path.
   Therefore targeted addon revisions remove addon-generated excess work,
   but cannot promise no unrelated provider executions after core events.
   Make these extra executions cheap through cached output.
   [DirectoryProvider](https://github.com/xbmc/xbmc/blob/21.3-Omega/xbmc/guilib/listproviders/DirectoryProvider.cpp).
3. **Existing visibility handling matters.** The container skips provider
   updates when invisible and not focusable. Preserve those gates; do not
   assert every hidden section is being fetched without measurements.
   [GUIBaseContainer](https://github.com/xbmc/xbmc/blob/21.3-Omega/xbmc/guilib/GUIBaseContainer.cpp).
4. **Artwork caching already belongs to Kodi.** Stable image URLs reuse
   its texture cache; changed artwork needs an image-specific version.
   Avoid global cache deletion or changing every image URL on each refresh.
   [Artwork cache](https://kodi.wiki/view/Artwork/Cache).
5. **Plugin ListItems support avoiding GUI locks.** Offscreen listing items
   can use `offscreen=True`; manually managed dialog items have different
   locking requirements.
   [ListItem API](https://xbmc.github.io/docs.kodi.tv/master/kodi-dev-kit/group__python__xbmcgui__listitem.html).
6. **Installation temporarily unloads the addon.** ZIP installation uses
   a background core job. Kodi unloads the addon, stages extracted files,
   swaps directories, then reloads it. The addon cannot report progress
   throughout its own unloaded phase. Inspect the native installer progress
   surface first; addon-owned skin-copy progress is a separate phase.
   The user's apparent deletion needs installation logs to distinguish
   unloading, staged replacement and an actual failure.
   [AddonInstaller](https://github.com/xbmc/xbmc/blob/21.3-Omega/xbmc/addons/AddonInstaller.cpp),
   [FilesystemInstaller](https://github.com/xbmc/xbmc/blob/21.3-Omega/xbmc/addons/FilesystemInstaller.cpp).

## Proposed refresh and cache contract

1. A successful local mutation commits its state, pending mirror work and
   dependency revision together. A no-op leaves revisions unchanged.
2. Revisions persist across restarts. Initialize the Home properties before
   exposing rows; startup itself is not a content change. Cache keys include
   row, parameters, language, user/provider scope, date where relevant and
   output-format version. Account switches cannot reuse another account's
   private data. Keep item action URLs stable across content-only refreshes
   to preserve focus.
3. A row serves a valid cached plain snapshot first. Local card rebuilding
   is bounded; network fetching never occurs on the widget path. Missing
   data queues a deduplicated service job. Do not enable caching of transient
   empty/missing results as a permanent successful response.
4. The worker prepares a complete generation while readers keep the old
   one. Successful empty content is a valid new generation; failure retains
   the old one. Publish atomically, with unique temporary files or an SQLite
   transaction and one writer per source.
5. Coalesce a short burst of events. Rebuild only dependent rows; compare
   effective visible output and publish only changed row revisions. Do not
   reread every unrelated row just to discover it stayed unchanged. Prioritize
   the visible section; prepare hidden affected rows later or on demand.
6. Mirroring runs after local feedback, with last-change ordering and durable
   retries. External Kodi adoption must not overwrite locally committed
   changes whose mirror is still pending. Check successful versions before
   recording completion; an old mirror must not clear newer pending work.
7. Network TTL, local-state change, artwork change and calendar/day change
   are separate triggers. A new episode can become aired at midnight without
   a user action; that transition must update the relevant local rows.
8. Builtin refreshes remain for compatible media windows, but blanket
   Container.Refresh/ReloadSkin is not the normal Home mutation mechanism.
   Native cache entries for descendant listings must also be invalidated or
   versioned when their state or inherited menu configuration changes.

### Row dependency coverage (all 38 Home types)

| Rows | Required change sources |
|---|---|
| cont_movies, my_movies_progress | Movie resume/watch/hide state, title metadata, row options |
| my_movies_added, my_movies_unwatched, my_movies_watched, my_movies_all | Movie membership/order, movie watch/activity state, metadata, options |
| my_movies_genres | Movie membership and genre metadata; not episode progress |
| cont_series | Series activity/hide state, episode resume/watch state, season metadata, date, options |
| my_series_next, my_series_new, my_series_upcoming | Series membership, episode state/availability, season metadata, date, options |
| my_series_added, my_series_unwatched, my_series_all | Series membership/order, applicable state/counts, metadata, options |
| my_series_genres | Series membership and genre metadata; not movie progress |
| recent_played | Own library play log, displayed title/episode metadata |
| widget_mytv_channels | TV channel membership/name/order/icon and row menu/click options |
| widget_mytv_next | TV channel series membership, channel progress, episode metadata and date |
| widget_mycinema_channels | Cinema membership/name/order/icon and row menu/click options |
| widget_mycinema_recommended | Cinema source movie set, provider/language, recommendation cache generation |
| widget_game_topics | Legacy recent history, own movie/series membership/metadata, stable random generation |
| widget_ai_recent | Recommendation history, options; watch-state publication when exposed by its menu |
| widget_kraska_root | Provider configuration and root cache generation |
| widget_tmdb_popular_movies, widget_tmdb_popular_series | Respective provider/language/cache generation and options |
| widget_csfd_charts | Static entries, localization and output-format version |
| vinyl_curator, vinyl_discover | Separate featured source generation |
| vinyl_songs, vinyl_albums, vinyl_artists, vinyl_playlists, vinyl_genres, vinyl_radios | Respective source generation, account, row options and image versions |
| vinyl_favorite_songs, vinyl_favorite_playlists, vinyl_favorite_albums, vinyl_favorite_artists | Respective favorites/source changes, account and row options |

Search movie/TV/person/history rows depend on the query generation. The two
local-library search rows additionally depend on their own membership/state
and metadata. Watch-state changes must update relevant properties/menus even
if the same search query remains open. Inherited Kodi video/music/PVR/addon/
favourite rows retain their native source contracts and native refreshes.

## Proposed implementation order and acceptance

| Phase | Change | Concrete acceptance |
|---|---|---|
| A | Local watched batching and durable asynchronous mirror | Cached manual mark completes its local commit before any Kodi RPC; 120-episode mark uses one write transaction; repeated manual mark is a no-op. |
| B | Dependency events, revisions and mutation coverage | Movie change does not change TV/Vinyl/TMDb row revisions; MyTV rename/progress refreshes every dependent slot, including Continue; no-op writes cause no refresh. |
| C | Bulk data reads, early limits, offscreen items, persistent row snapshots | 100 movies/limit 10 creates at most 10 ListItems, with a bounded handful of DB reads; warm widget paths make zero provider calls. |
| D | Reliable queued refresh, successful empty results, failures/recovery | Delete/empty result appears correctly; network failure retains old content; offline restart has cached rows; partial season metadata retries; mirror survives restart. |
| E | Source-specific Vinyl and MyCinema work | One favorite edit does not download all songs; duplicate cinema movie IDs produce one recommendation fetch; disabled unused sources create no routine network work. |
| F | Package and skin-copy progress/recovery | Packaged screenshot declarations and files agree; runtime assets remain present; skin-copy progress/result is visible; interrupted swap recovers. Native addon install is diagnosed separately. |
| G | Optional weak-device display profile | Default appearance preserved unless approved; lightweight option uses 10–20 items, fewer visible rows, smaller appropriate artwork and reduced effects. Measure scrolling and section switching on the target. |

Proposed UX targets for later measurement: cached single-item feedback
within roughly 250 ms, affected visible row update within roughly 500 ms,
warm visible Home content within roughly one second after skin readiness.
These are targets, not verified results; uncached first-ever startup and
missing series coverage need their own progress and completion semantics.

Necessary focused verification when implementation is authorized: counts
and dependency tests offline; failure, no-op, empty-result, account-switch,
midnight and restart cases; then explicitly authorized live checks of only
changed screens on Kodi and the weakest target device. Record plugin startup,
row preparation, rendering/artwork readiness and installation phases
separately. The current widget timers start after imports and omit significant
startup/context-decoration/native-display work.

## Remaining validation

The user approved the complete implementation and a clean final deployment.
The weakest target is an approximately eight-year-old Android TV; its exact
model, RAM and Kodi version remain unspecified. Installation is through Kodi's
Install from ZIP. Neither native ZIP-install duration nor target device timing
has been measured. The Mac development symlink cannot reproduce production
copying. Kodi controls the addon ZIP extraction/unload phase; addon progress
covers the subsequent bundled-skin copy and migration.

Native resource loading was checked against Kodi 21.3 source:
[Texture.cpp](https://github.com/xbmc/xbmc/blob/21.3-Omega/xbmc/guilib/Texture.cpp),
[GUIFontManager.cpp](https://github.com/xbmc/xbmc/blob/21.3-Omega/xbmc/guilib/GUIFontManager.cpp),
[GUIInfoLabel.cpp](https://github.com/xbmc/xbmc/blob/21.3-Omega/xbmc/guilib/guiinfo/GUIInfoLabel.cpp).
The external XBT URL encodes the translated runtime path. Infolabel prefix
strings are literal: dynamic native texture paths must keep the Home property
outside a nested `$INFO` prefix. No GUI appearance claim follows from this
source verification.

Skin metadata uses the exact sibling-relative logo path because
[AddonInfoBuilder](https://github.com/xbmc/xbmc/blob/21.3-Omega/xbmc/addons/addoninfo/AddonInfoBuilder.cpp)
always joins metadata asset paths to the addon directory. The bundled skin
declares the parent addon as a dependency. Kodi stops and starts addon services
on reinstall, preserving the profile, as confirmed in
[Service.cpp](https://github.com/xbmc/xbmc/blob/21.3-Omega/xbmc/addons/Service.cpp).
