buildSubtitleImport nested the metadata loop inside the import loop and reused
the outer counter $i. The inner loop advanced $i to count(files), so the outer
import loop exited after its first iteration: only the first subtitle was
imported (-sub_charenc -i), while -map/-metadata was still emitted for every
file — mapping ffmpeg inputs that were never added. Multi-subtitle movies got a
single imported track plus dangling -map references (broken output).
Split the nested loop into two siblings — import every file, then map every
file — and cache the count. All configured subtitles are now imported and
mapped to the correct input index.
Add tests/Unit/StreamProcessSubtitleTest.php covering empty input, a single
local subtitle, remote-server fetch, and the multi-subtitle case that
reproduces the defect (import count was 1, must equal N). Verified red→green
against the pre-fix code.
Verified: php -l clean, make gates green, phpunit 336/336 green.
Move the VOD subtitle import + metadata assembly out of startMovie into a
private helper so the method is smaller and the subtitle logic is isolated.
Behaviour is preserved verbatim, INCLUDING a pre-existing bug: the inner
metadata loop reuses the outer import loop's $i counter, so the import loop
effectively runs once (first file) while metadata is mapped for all files.
Left as-is here; it is a real defect to fix in a dedicated change.
Verified: php -l clean, make gates green.
createChannelItem, startMovie and startStream each inlined the same blocks
verbatim. Extract them into private helpers — removing the duplication and
shrinking the three oversized methods:
- buildLogoFilterOptions() — logo / -filter_complex overlay block (was inlined 3x)
- resolveGpuInputCodec() — CUVID hardware-decoder detection (was inlined 2x)
- applyDefaultCopyCodecs() — default -acodec/-vcodec to stream copy (was inlined 3x)
Pure extraction, behaviour preserved. startStream's GPU block is intentionally
left inline: it reuses the already-computed $rFFProbeOutput instead of
re-probing the source, so it is not the same code.
Verified: php -l clean, make gates green (PSR-4 use-imports, LB manifest,
vendor prod-only). net -28 lines.
The Stream Info column always showed a transcode speed of "1x" even though
ffmpeg's real speed fluctuates. Two independent bugs were involved.
1. Progress was never captured while a stream ran.
ffmpeg's `-progress` posted to the HTTP /progress endpoint, but ffmpeg
holds one open-ended chunked POST for the stream's whole life. PHP-FPM's
php://input can't read a length-less body, and nginx only invokes PHP
once the request body is complete, so the endpoint fired only at stream
end. `progress_info` therefore stayed empty and the view fell back to the
hardcoded "1x".
Switch ffmpeg to write `-progress` to a local file (the mechanism the
created-channel path already uses). StreamsCronJob now tails that file,
keeps the last complete report block, re-encodes it as the JSON the panel
expects, and truncates it each cycle so it stays tiny on disk.
2. Speeds just above 1 were swallowed by the display.
`floor($speed * 100) / 100` dropped e.g. 1.01 to "1x" because 1.01 * 100
is 100.9999… in floating point. Use round() instead, and apply the same
robust parsing to the radios path (which multiplied the "1.01x" string).
Files:
- Domain/Stream/StreamProcess.php ffmpeg -progress -> local file
- Cli/CronJobs/StreamsCronJob.php tail/parse/truncate the progress file
- Public/Controllers/Admin/TableController.php round() speed display
Null-coalesce absent keys in activity info, cached signal counts, streams_servers
maps, server protocol and PID-running results, and @-suppress the stale .pid
unlink. Avoids offset-on-null/undefined-key warnings under PHP 8 without changing
behaviour on present data.
- QueueCommand: adopt a live build PID instead of unlinking its .create
marker and relaunching (which killed the running encoder and reset the
build to 0% on every queue re-add); widen the PID capture window from
0.3s to 5s to survive console.php bootstrap
- VodCronJob: check build liveness locally via /proc cmdline instead of an
HTTP self-call that falsely reported the build dead whenever the internal
API or server_online flag hiccuped
- CreatedCommand: verbose CLI output (per-source status, encode % / speed
every 10s), local process checks instead of HTTP, cleanup of stale
_.create on the nothing-to-build early exit
- StreamProcess: pass -progress <id>_<md5>.progress to the channel-item
ffmpeg command; CreatedCommand parses it and mirrors the percentage into
streams_servers.progress_info
- TableController/api.php: "% DONE" now includes fractional progress of the
source currently being encoded
- MonitorCommand: stop type-3 (created) channels from falling through into
the LLOD branch after startStream — LLOD cannot consume raw
"s:<server>:<path>" sources and spammed "[LLOD] Invalid source" in a
restart loop; the on-demand LLOD shortcut now also skips type 3
- PHP 8 warning fixes along the same chain: missing on_demand key
(ChannelService), undefined $rSplit and missing parent_id column in
grouped queries (TableController), missing .create file (VodCronJob),
empty lines in _.list parsing and null cc_info (StreamProcess)
- StreamProcess: parse_url() output has no 'port'/'path' keys for default-port
URLs — probe URL built with isset() guards (477 warnings/8h in panel logs)
- StreamProcess: json_decode of transcode_attributes/profile_options can
return null — normalize to array before array_key_exists/array_merge
(TypeError at stream start)
- MonitorCommand: the .m3u8 playlist can vanish between the loop's
file_exists check and md5_file (stream restart race) — guard both call sites
Replace the fragile per-class setDb()/db() pattern (which threw when a
bootstrap path forgot to wire $db) with a shared trait that lazily resolves
from DatabaseFactory. Fixes the streaming UserRepository fatal and the same
latent issue in module crons. 42 classes converted.
- Exclude bundled third-party code from analysis (Modules/tmdb/lib/*,
Core/Util/MobileDetect.php) — like the vendored Composer packages, these
are not our code (php-tmdb client, Mobile-Detect). Removes 67 baselined
errors incl. the only class.notFound and phpDoc.parseError (both vendored).
- return.void (10): drop the discarded return value (return true/false/null →
return;) in void methods — NetworkUtils, LineService, StreamProcess, TMDbService.
- deadCode.unreachable (2): remove the dead `return;` after AdminHelpers::goHome()
(a never-returning call) in Plex/WatchController.
- Regenerate phpstan-baseline.neon: 440 -> 361 errors (291 -> 241 blocks).
Verified: make phpstan green, PHPUnit 298/298.
Apply 'make cs-fix' — 493 files. Mechanical, import-block only:
- sort use statements alphabetically (class/function/const grouped);
- drop imports left unused by the PSR-4 migration (e.g. classes referenced by
leading-backslash FQCN whose redundant 'use' the automated insertion had added);
- one blank line after namespace and after the import block; collapse stray
blank lines around use.
No logic changes. Verified: php -l clean; PHPStan no errors; PHPUnit 295/295; and a
temporary PHPStan pass over src/Public/Controllers confirms no still-referenced
import was removed (0 unresolved classes). 'use' after inline HTML in view
templates is valid and aliases correctly (verified) — those imports are sorted too.
Move Core/Http into XcVm\Core\Http (ApiClient, CurlClient, RequestManager,
Request, Response, Router) and Core/Http/Pipeline into XcVm\Core\Http\Pipeline
(StreamContext, StreamMiddlewareInterface, StreamPipeline). RequestGuard.php is
procedural (global functions) and stays global.
- Namespace the 9 classes; qualify still-global deps with leading backslash
(\ServerRepository in ApiClient; \Authorization, \ServiceContainer,
\AdminHelpers in Router) and built-in \Throwable; same-namespace siblings
unqualified.
- Add 'use XcVm\Core\Http\...;' to referencing files — RequestManager 128,
Router 19, ApiClient 15, Request 11, Response 6, CurlClient 5, plus Pipeline —
across src/ and tests/.
- Rewrite the modules' 'use Router;' → 'use XcVm\Core\Http\Router;' (plex,
watch, tmdb).
- Fix pre-existing leading-backslash global refs (\Router etc.) to the FQCN.
- Tests: fully-qualify the Router type hint in generated module fixtures
(registerRoutes(\XcVm\Core\Http\Router ...)) and update the
InterfaceContractTest param-type expectation to the FQCN. Also repaired two
fixtures where the use-inserter mis-placed a 'use' (files declare a method
literally named namespace(), which the tokenizer reports as T_NAMESPACE).
- phpstan-baseline.neon regenerated (292→292; class names in frozen messages
gained the namespace, no new/unknown-class errors).
Verified: php -l clean; PHPStan no errors; PHPUnit 295/295; Http classes resolve
via Composer; leading-backslash re-sweep across migrated classes is clean.
Move Core/Database into XcVm\Core\Database (DatabaseHandler, Database,
MigrationRunner, QueryHelper). First of the Core hub sub-layers.
- Namespace the 4 classes; DatabaseHandler extends Database (same namespace);
built-ins/ioncube qualified (\PDO, \PDOException, \Exception, \Throwable,
\XC_VM); Database keeps its 'use XcVm\Core\Logging\FileLogger;'.
- Add 'use XcVm\Core\Database\...;' to referencing files (DatabaseHandler 51,
Database 64, QueryHelper 29, MigrationRunner 3) + 2 test files (PHPStan does not
analyse tests/, so PHPUnit is the gate there).
- Rewrite pre-existing leading-backslash global refs (\DatabaseHandler etc., e.g.
in @param docblocks of ResellerApiDispatcher/ResellerTableRenderer) to the full
FQCN \XcVm\Core\Database\... — a 'use' import does not cover a leading-\
reference. Done with a lookbehind so FQCN continuations and use-lines are intact.
- phpstan-baseline.neon regenerated (292→292; pre-existing Database/migration_logic
findings re-anchored after class names in messages gained the namespace).
Verified: php -l clean; PHPStan no errors; PHPUnit 295/295.
Move the last two Core/Config classes into XcVm\Core\Config. SettingsManager
has the largest fan-out of the whole migration (referenced by ~224 files).
- Namespace SettingsManager (self-contained singleton, no class deps) and
SettingsRepository (\FileCache:: qualified).
- Add 'use XcVm\Core\Config\SettingsManager;' to 224 referencing files and
'use ...\SettingsRepository;' to 12 — call sites (SettingsManager::get(), etc.)
unchanged. Done with a token-based inserter (after namespace/declare/<?php,
idempotent, same-namespace files skipped).
- ToolsCommand::processRecaptcha: drop dead class_exists('SettingsManager') +
method_exists guard (always autoloadable now) → call SettingsManager::clearCache()
directly. This was the only string-literal class reference.
Core/Config is now fully namespaced (ConfigReader, DomainResolver, SettingsManager,
SettingsRepository); the procedural constant files (AppConfig/Binaries/Paths)
remain global by design.
Verified: php -l clean (226 files); PHPStan no errors (baseline unchanged — it is
line-independent so the added use-lines don't disturb it); PHPUnit 295/295; all
Config FQCNs resolve via Composer; sample Public referrers lint-clean.