The delayed-HLS branch inline-computed two things: the resume segment number
parsed from the existing delay playlist (last-or-previous line's _<n>.ts index),
and the delay sleep seconds (delay_minutes*60 reduced ~10s per already-produced
segment, floored at 0). Pull both into pure resolveDelaySegmentStart() and
resolveDelaySleepTime() helpers with unit tests. The playlist merge and the
short-playlist abort stay inline. No behaviour change.
Verified: phpstan level 5 green, phpunit 391/391, make gates green.
The ffprobe-skip branch of the source loop hand-rolled two things: a scan of the
stream arguments for skip_ffprobe == 1, and the literal assumed h264/aac mpegts
result used when the scan matched. Give both names via pure helpers and cover
them with unit tests. No behaviour change.
Verified: phpstan level 5 green, phpunit 385/385, make gates green.
The {AAC_FILTER} substitution carried the rule inline: emit -bsf:a
aac_adtstoasc only when a copied AAC audio stream is muxed into a non-FLV
container. Give that rule a name and a test via a pure aacBitstreamFilter()
helper. No behaviour change.
Add unit tests: applied for copied AAC in mpegts/empty container, skipped for
FLV, skipped when the codec is not AAC or the output is not a copy.
Verified: phpstan level 5 green, phpunit 382/382, make gates green.
The codec-metadata derivation persisted for a started live stream (player
compatibility, audio/video codec names, resolution snapped to the nearest
standard height) was an inline block reading ffprobe output. Pull it into a
pure resolveStreamCodecMeta() helper taking the ffprobe array and the
player_allow_hevc flag and returning [compatible, audio, video, resolution].
The only external input, SettingsManager::getAll()['player_allow_hevc'], is now
passed in (getAll() just returns the cached settings array, so hoisting the read
out of the guard is free). Add unit tests: non-array/missing-codecs defaults,
compatible h264/aac, resolution snapping, HEVC gating by the allow flag, and
incompatible-but-reported codecs.
Verified: phpstan level 5 green, phpunit 379/379, make gates green.
The X-XC_VM-Detect / X-XC_VM-Prebuffer header injection was duplicated three
times in startStream: each block looped the argument list to append the header
to an existing 'headers' entry and added a new one if absent. Collapse the three
copies into a single appendHeaderArgument() helper and drop the shared
$rProcessed flag they threaded through (it leaked state between the blocks).
Behaviour is unchanged: the helper uses a local applied flag, and the three call
sites produce the same argument lists as before. Add unit tests covering
append-to-existing, create-when-absent, append-to-every-headers-entry, and the
empty-list case.
Verified: phpstan level 5 green, phpunit 373/373, make gates green.
Continue breaking down StreamProcess::startStream:
- extract resolveProbeSettings() (ffprobe/analysis timing) and
rotateSourcesPastCurrent() (source-failover ordering) as private static
helpers, with unit tests locking in their behaviour;
- simplify 15 inverted empty-if/else blocks across the class into a single
positive condition (no behaviour change).
Also fix the latent undefined-variable bugs the refactor surfaced under PHPStan:
startStream assembles the ffmpeg command from state produced inside the source
failover loop (which closes before the command is built) and inside the
non-custom_ffmpeg branch. On an empty source list, or when a custom ffmpeg line
is used, several variables were read while possibly undefined -- masked at
runtime only by ternary guards and suppressed in the PHPStan baseline. Default
them at the two scopes that own them:
- loop-scoped, read after the loop: $rSource, $rRealSource, $rStreamSource,
$rProtocol, $rFFProbeOutput
- non-custom_ffmpeg-branch-scoped, read in the {MAP}/{GEN_PTS}/{READ_NATIVE}
substitution and delay sleep: $rMap, $rGenPTS, $rReadNative, $rSleepTime
and drop the 9 now-obsolete entries from phpstan-baseline.neon.
Behaviour is unchanged on the happy path (the loop/branch overwrite the
defaults) and the defaults match the existing runtime ternary-guard results.
Verified: phpstan level 5 green (whole project, entries un-suppressed),
phpunit 369/369, make gates green.
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.
The top-right profile dropdown was a hardcoded HTML block in
admin/header.php, so modules could not add or remove entries and the
plex/watch "settings" links were baked into core.
Move the menu into the NavbarRegistry: CoreNavbarProvider now registers
the core items (edit profile, settings, backups, cache, modules, logout)
under a reserved 'profile' parent, and header.php renders
NavbarRegistry::getChildren('profile') adaptively. There is intentionally
no top-level 'profile' node, so the items never leak into the main
navigation. Order 100–980 is reserved for module-provided links.
Extract NavbarRegistry::collapseDividers() to drop separators orphaned by
permission filtering (leading, trailing, consecutive), and reuse the
shared _xc_nav_visible()/_xc_nav_label() helpers by defining them once
near the top of the header. Remove the hardcoded settings_plex /
settings_watch entries — those now belong to their owning modules.
Add NavbarProfileMenuTest covering collapseDividers, the core 'profile'
contract (no top-level leak, edit first / logout last, permission gates),
and the module-integration contract (reserved order slots, divider
collapse when folder_watch_settings is absent).
The panel is deeply coupled to TMDb (VOD import, player metadata, admin
search, two crons), so shipping it as an uninstallable module only added
failure modes: after the move to hash-suffixed dirs (tmdb_f4e6e) every
hardcoded `Modules/tmdb/lib/...` require broke, and 2.3.3 crons died with
"Failed opening required TmdbClient.php".
tmdb -> core:
- Vendored \TMDB client -> src/Infrastructure/Tmdb/lib/; the only loader
is TmdbApiService::requireLibrary() (now public, also loads Release.php).
- TmdbApiService -> XcVm\Infrastructure\Tmdb — composer-autoloaded in every
bootstrap context, no module boot required (player scope never booted
modules, so module-namespace classes were unreachable there).
- TmdbCron / TmdbPopularCron -> XcVm\Domain\Vod; cron jobs -> Cli/CronJobs
(picked up by the console.php scan; command names cron:tmdb and
cron:tmdb_popular are unchanged).
- TmdbController -> Public/Controllers/Admin; tmdb_search / tmdb api
actions registered in routes/admin.php (same dispatchApi fallback).
- Domain/Vod services and player_functions.php load the lib through
TMDbService::requireLibrary() instead of hardcoded module paths.
- tmdb removed from config/bundled_modules.php. ModuleLoader gains
CORE_PROVIDED_MODULES: released watch/plex archives still declare
"dependencies": ["tmdb"] — such deps are stripped during manifest
normalization and in ModuleManager::listModules().
- syncBundledModules() purges stale on-disk tmdb module dirs and their
config/modules.php state on upgraded panels, so the old copy cannot boot
alongside the core implementation and collide on command names.
Standard-set provisioning fix (root cause of the "Undefined variable $db"
errors from watch/plex settings views on 2.3.3):
- Production still ran watch_e6c86/plex_20cd9-less legacy copies migrated
from 2.3.2 with generated hash_ids; provisionStandardSet() treated any
same-name directory as "already on disk" and never fetched the pinned
1.0.2/1.0.1 releases that contain the fix. A same-name directory whose
identity does not match the pinned hash_id is now considered stale: it
is deleted and the pinned release is installed in its place.
- installModuleFromSource(): when the module is already recorded as
installed (files re-provisioned over a stale copy), run updateModule()
(incremental from->to migrations) instead of re-running the initial
install.
- ArchitectureTest/InterfaceContractTest resolve modules via module.json instead
of the directory basename (works with {name}_{hash5}).
- ModuleLoaderTest: loads from a hash-suffixed dir; a broken module and its
dependents are skipped without aborting the whole load.
- ModuleManagerMigrationsTest: legacy bare -> hashed migration (+ stale-dup drop),
and install of a module living in a hash-suffixed directory.
ModuleUpdateChecker resolves the latest available version per module by source:
git (via GitHubReleases), url (version.json), platform (best-effort), bundled
(no-op). ModuleUpdatesCronJob (cron:module_updates) records available_version
into config/modules.php; migration 008 registers it to run weekly. Network-free
unit tests cover the routing.
The XC_Autoloader fallback was already retired (no-op stub); this deletes it for
good. Resolution is now 100% Composer PSR-4 (+ ModuleLoader for modules), no
legacy scanner, no class-map cache.
- Move `define('MAIN_HOME', ...)` into bootstrap.php (autoload.php used to define
it); bootstrap.php now requires only vendor/autoload.php.
- Drop `\XC_Autoloader::clearCache()/warmCache()` from StartupCommand.
- tests/bootstrap.php: locate-guard + require switched to vendor/autoload.php.
- Entry points that required autoload.php directly — Public/index.php,
Public/admin/index.php, Public/stream/index.php, Public/progress/index.php,
ministra/portal.php and Admin/Reseller TableController — switched to
vendor/autoload.php (defining MAIN_HOME where they did not already). These were
not in the plan's checklist; found via grep during execution.
- phpstan.dist.neon: drop src/autoload.php from scanFiles.
- Makefile: drop autoload.php from LB_ROOT_FILES.
- deleted_files.txt: add autoload.php (client cleanup on update).
- AutoloadOrderTest: now asserts the XC_Autoloader class and file are gone.
- git rm src/autoload.php.
- PSR4_MIGRATION_PLAN.md: mark final-phase step 2 done.
Verified: grep XC_Autoloader:: = 0; php -l clean; PHPStan no errors; PHPUnit
303/303; make gates pass; bootstrap smoke — MAIN_HOME + XC_Bootstrap present,
XC_Autoloader gone, only the Composer autoloader registered.
enigma, episode, episodes, process_monitor and stream_view referenced migrated
classes (UserRepository, BouquetService, StreamRepository, RequestManager,
SettingsManager, ...) by short name with either no `use` or a `use` placed
*below* the first usage. PHP imports outside the top scope are positional, so the
short name resolved to a now-nonexistent global class — a runtime fatal on those
admin pages (php -l passes; not covered by PHPStan/PHPUnit). The migration's
automated `use` insertion missed them due to the interleaved HTML / short-tag
(<? , <?=) structure, and the later php-cs-fixer pass stripped some as 'unused'
because it could not see usage inside short-tag blocks.
- Consolidate every needed `use XcVm\...;` into a single top-of-file PHP block.
- Exclude Public/Views and Modules/*/views from php-cs-fixer (no_unused_imports
is unreliable on short-tag templates); their import correctness is enforced by
the new check_procedural_use gate instead.
Verified: php -l (short_open_tag=1) clean; PHPStan no errors; PHPUnit green.
Namespace the modules' own classes into XcVm\Module\<Pascal> (Plex, Watch, Tmdb,
Ministra) — PlexController/PlexService/PlexCron/..., WatchController/WatchService/...,
TmdbController/TmdbCron/TmdbApiService/..., PortalHandler/PortalHelpers (~23 classes).
They now resolve through the Phase-2 ModuleLoader PSR-4 resolver.
- The bundled third-party tmdb/lib/* (TMDB client, Entities, roles, config) stays
GLOBAL, like the other vendored libs; namespaced Tmdb classes reference it via
\TMDB etc.
- Rewrite the *Module classes' (and any sibling) 'use ShortName;' imports → FQCN;
add use to referrers; convert leading-backslash refs. Sub-classes keep the
Core/Domain/Cli use imports added during those layers.
- Qualify built-ins (\DateTime, \Exception, \PDO, ...) and the global lib classes.
- phpstan-baseline.neon regenerated.
Verified: php -l clean; PHPStan no errors; PHPUnit 295/295; all 4 modules load and
their sub-classes (e.g. XcVm\Module\Plex\PlexService) resolve via the module
PSR-4 autoloader; re-sweep clean.
Namespace all 55 Cli classes into XcVm\Cli (CommandInterface, CommandRegistry,
CronTrait, DaemonTrait), XcVm\Cli\Commands (28) and XcVm\Cli\CronJobs (23);
migration_logic.php stays procedural/global.
- console.php: switch command discovery from basename==classname + manual require
to FQCN resolution — each scan dir maps to its PSR-4 namespace, classes load via
Composer, implementsInterface(CommandInterface::class). Drop the manual
CommandInterface/CommandRegistry requires. This is the atomic switch the plan
required to land with the Cli namespacing.
- Commands/CronJobs get 'use XcVm\Cli\CommandInterface;' (implements) and the
trait imports 'use XcVm\Cli\CronTrait;'/'DaemonTrait;'; rewrite the modules'
'use CommandRegistry;' → FQCN; qualify built-ins/global (\ReflectionClass,
\PDO, \Exception, \RuntimeException, \XC_Autoloader, \XC_VM).
- Fixtures: 'use CommandRegistry;' → FQCN; InterfaceContractTest registerCommands
param-type → FQCN. phpstan-baseline.neon regenerated.
Verified: php -l clean; PHPStan no errors; PHPUnit 295/295; FQCN discovery resolves
51 classes (49 implement CommandInterface); no mangled FQCNs; re-sweep clean.
Move Core/Events into XcVm\Core\Events and its sub-trees: \Contract
(StoppableEventInterface), \Auth, \Module, \Settings, \Stream (the event
classes), plus root EventDispatcher, ListenerProvider, ListensTo, AbstractEvent.
- Namespace 14 files across 6 namespaces; cross-namespace refs imported via use
(AbstractEvent/EventDispatcher → Contract\StoppableEventInterface; Stream events
→ root AbstractEvent). Built-in \Attribute (ListensTo) and ioncube \XC_VM
(PackageInstalledEvent) qualified.
- Rewrite 'use ListensTo;' → FQCN; add use to referrers across src/ and tests/;
fix leading-backslash refs (\EventDispatcher, \ListensTo, \PackageInstalledEvent
from the Module commit) to their FQCNs.
- Tests: add real top-level use imports to ModuleLoaderBootTest (EventDispatcher)
and ListensToAttributeTest (ListensTo) — the use-inserter skipped/mis-placed them
due to a namespace() method and a heredoc fixture already containing the FQCN.
- phpstan-baseline.neon regenerated (292→292).
Completes Core/Container + Core/Events. Verified: php -l clean; PHPStan no errors;
PHPUnit 295/295; EventDispatcher resolves and AbstractEvent implements
XcVm\Core\Events\Contract\StoppableEventInterface.
Move Core/Module into XcVm\Core\Module (BaseModule, ModuleInterface,
MigratableInterface, ModuleLoader, ModuleManager, NavbarRegistry, NavbarItem,
CoreNavbarProvider) and Core/Module/Contract into XcVm\Core\Module\Contract
(the 6 provider interfaces). Completes the Core hub layer.
- Namespace 14 files (8 root + 6 Contract). Root files import the contracts via
'use XcVm\Core\Module\Contract\...'; NavbarProviderInterface imports the
root NavbarRegistry. Qualify still-global deps with leading backslash
(\ServiceContainer, \CommandRegistry, \ModuleState, \ServerEnvironment,
\EventDispatcher, \ListensTo, the Module exceptions, \ZipArchive,
\InvalidArgumentException, \RuntimeException) — Container/Events/Enum/Exception
migrate later. Migrated deps (Router, StreamPipeline, ...) keep their use.
- Rewrite the modules' 'use BaseModule/NavbarRegistry/NavbarItem;' → FQCN; add
'use XcVm\Core\Module\...;' to other referrers across src/ and tests/.
- Fix pre-existing leading-backslash refs to the FQCN.
- Tests: qualify the module fixtures' generated 'use ModuleInterface;' /
'use BaseModule;' / 'use NavbarRegistry;' and the registerRoutes type hint to
FQCN; add real top-level use imports to the fixture-builder tests; update
InterfaceContractTest registerNavbar param-type to the FQCN. Repaired
use-inserter mis-placements in the two tests that declare a namespace() method.
- Public/index.php: class_exists('ModuleLoader') → class_exists(ModuleLoader::class).
- phpstan-baseline.neon regenerated (292→292; no new/unknown-class errors).
Verified: php -l clean; PHPStan no errors; PHPUnit 295/295; the 4 real modules
load and resolve as XcVm\Core\Module\ModuleInterface; leading-backslash
re-sweep across all migrated classes is clean.
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.
Rewrite ModuleLoader::registerModuleAutoloader() from a lossy short-name + glob
lookup into a true per-module PSR-4 resolver: the module's base namespace
(XcVm\Module\{Name}) maps onto its directory, and the namespace remainder
becomes the sub-path.
XcVm\Module\Watch\WatchModule → {modulePath}/WatchModule.php
XcVm\Module\Watch\Service\WatchService → {modulePath}/Service/WatchService.php
- Only classes under the module's own namespace are claimed; everything else
(global legacy classes, other modules, core) falls through untouched. This
removes the short-name glob, so two modules — or two sub-namespaces in one
module — can declare same-named classes without colliding.
- load() now derives the base namespace from the resolved FQCN and registers the
autoloader after class resolution (reordered, still before the main require).
- Marketplace slug dirs unaffected (real $modulePath is used); encrypted-file
handling preserved (require_once + zend_compile_file decrypt hook).
- resolveClassName()/load() already targeted XcVm\Module\{Pascal}\{Pascal}Module
(done in earlier work) — left as is.
console.php FQCN discovery is intentionally NOT changed here: the Cli layer is
still global (CommandInterface, *Command, *CronJob), so switching discovery to
\XcVm\Cli\... would break it. Per the plan it switches atomically with the Cli
layer namespacing (phases 3..N).
New test tests/Unit/ModuleLoaderPsr4ResolverTest.php: sub-namespace resolution,
same-short-name no-collision, foreign-namespace fall-through.
Verified: php -l clean; PHPUnit 295/295 (+3); PHPStan no errors; all 4 real
modules (plex/watch/tmdb/ministra) load and their *Module FQCNs resolve.
Adds tests/Support/TestDb.php — a Database-compatible wrapper over
sqlite::memory: injected via setDb() — and StreamConfigRepositoryTest as a
worked example (SELECT/list/keyed/DELETE-cascade/not-found). 286 -> 292 tests.
- Added new fields to module.json: environment, dependencies, has_navbar, and has_settings.
- Updated ModuleLoader to support environment filtering and topological sorting of modules based on dependencies.
- Implemented error handling for missing and cyclic dependencies during module loading.
- Enhanced documentation for module.json structure and ModuleLoader functionality.
- Introduced unit tests for ModuleLoader to validate loading behavior and dependency resolution.
- Updated existing modules' manifest files to include new fields.
Co-authored-by: Copilot <copilot@github.com>
- Added `StreamInterface` to define the contract for stream handling.
- Implemented `FileStream` class for file-based streaming.
- Implemented `TextStream` class for in-memory text streaming.
- Removed legacy `M3UParser` class and related resources.
- Added new tests for `AttributeStringToArray`, `Iso8601Transformer`, `Resolution`, `Byterange`, `Inf`, `Line`, and `Lines` classes.
- Updated bootstrap file to include new PhpM3u8 components.
- Replace legacy in-project M3U parser with vendored Gemorroj/M3uParser
- Initialize parser via bootstrap with proper namespacing
- Remove obsolete src/domain/Stream/M3UEntry.php implementation
- Update stream import/review mapping:
- use logo as fallback when tvg-logo is missing
- Add third-party compliance files for vendored parser:
- ATTRIBUTION.md
- LGPL LICENSE
- Add full M3uParser unit test suite:
- tag parsing
- parser behavior
- fixtures
- Add StreamService integration tests for M3U parsing/import flow
- Update test bootstrap:
- add igbinary_serialize / igbinary_unserialize polyfills
- explicitly load M3uParser bootstrap and custom test tag class
- add PHPUnit bootstrap and config under tests/
- add focused unit tests for GitHubReleases, InputValidator and FfmpegPaths
- fix GitHubReleases cache file handling when switching update channel
- document PHPUnit PHAR usage and debug run commands in RU/EN docs
- document SFTP sync for tests/ and contributor test workflow
- remove broad smoke coverage approach in favor of per-file tests