diff --git a/.env.example b/.env.example index ece332b..fe06b61 100644 --- a/.env.example +++ b/.env.example @@ -37,3 +37,15 @@ AUTO_UPDATE_ENABLED=false AUTO_UPDATE_INTERVAL_MINUTES=60 GIT_REMOTE=origin GIT_BRANCH=main + +# Lumi host operator identity shown in generated Companion pairing packages. +# Set these when distributing Companion from a self-hosted Lumi installation. +LUMI_OPERATOR_NAME= +LUMI_OPERATOR_CONTACT= +LUMI_OPERATOR_PRIVACY_URL= + +# Runtime mode and bind host +# Lumi auto-detects development mode from NODE_ENV, a loopback bind host, or a +# source checkout. Explicit overrides always win. +# LUMI_DEV_MODE=true +# LUMI_HOST=127.0.0.1 diff --git a/.gitignore b/.gitignore index 2ab289f..882a0a0 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ Twitch.png twitch-credentials-lumi.png .secrets .secrets-* +DEVNOTES.md \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a57641..36b0add 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Lumi changelog +## 0.2.26 + +- Added stable Lumi Companion distribution with a durable per-user installer, shared paired-device authentication, managed OBS integration, preserved identity, user-approved updates, complete legal notices, and matching application/tray branding. +- Added server-hosted transcription with voice-free readiness checks, dedicated confidence and latency benchmarks, resilient worker recovery, detailed WebUI analysis, and no speech inference on the streaming computer. +- Added Spotify-backed Song Overlay capture, transparent and live-updating OBS rendering, configurable announcements, and a shared-template `!music` command. +- Made stable core updates snapshot, synchronize, verify, and restore bundled plugins as one transaction while preserving plugin data and local-only plugins, so hosts on `0.2.25` receive the full release in one update. +- Stopped notification-area flicker by caching health icons, coalescing queued runtime state, and avoiding unchanged native icon/menu assignments. + ## 0.2.25 - Added verified, snapshot-backed production deployment switching between `main` and exact `experimental-*` branches, including bundled plugin code, preserved plugin data, automatic failed-apply restoration, and graceful wrapper restarts. diff --git a/companion/Lumi.Companion.sln b/companion/Lumi.Companion.sln index 3463a21..8bcde2d 100644 --- a/companion/Lumi.Companion.sln +++ b/companion/Lumi.Companion.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lumi.Companion.Abstractions", "src\Lumi.Companion.Abstractions\Lumi.Companion.Abstractions.csproj", "{0B3C2C62-2CCB-43C9-9AF9-769AC7F22285}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lumi.Companion.Protocol", "src\Lumi.Companion.Protocol\Lumi.Companion.Protocol.csproj", "{3703AF57-828D-4E2F-BFBA-A95833555A5A}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lumi.Companion.Core", "src\Lumi.Companion.Core\Lumi.Companion.Core.csproj", "{0BDAE25E-C487-456D-A465-41A0CA48AF30}" @@ -17,6 +19,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "plugins", "plugins", "{07D5 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lumi.Companion.Transcription", "plugins\Lumi.Companion.Transcription\Lumi.Companion.Transcription.csproj", "{49C19CA8-4669-4C33-B3A8-9CB934CD1171}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lumi.Companion.SongOverlay", "plugins\Lumi.Companion.SongOverlay\Lumi.Companion.SongOverlay.csproj", "{F72BAFAD-17D3-4EF2-8690-B3B5646556C2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +31,30 @@ Global Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0B3C2C62-2CCB-43C9-9AF9-769AC7F22285}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0B3C2C62-2CCB-43C9-9AF9-769AC7F22285}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0B3C2C62-2CCB-43C9-9AF9-769AC7F22285}.Debug|x64.ActiveCfg = Debug|Any CPU + {0B3C2C62-2CCB-43C9-9AF9-769AC7F22285}.Debug|x64.Build.0 = Debug|Any CPU + {0B3C2C62-2CCB-43C9-9AF9-769AC7F22285}.Debug|x86.ActiveCfg = Debug|Any CPU + {0B3C2C62-2CCB-43C9-9AF9-769AC7F22285}.Debug|x86.Build.0 = Debug|Any CPU + {0B3C2C62-2CCB-43C9-9AF9-769AC7F22285}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0B3C2C62-2CCB-43C9-9AF9-769AC7F22285}.Release|Any CPU.Build.0 = Release|Any CPU + {0B3C2C62-2CCB-43C9-9AF9-769AC7F22285}.Release|x64.ActiveCfg = Release|Any CPU + {0B3C2C62-2CCB-43C9-9AF9-769AC7F22285}.Release|x64.Build.0 = Release|Any CPU + {0B3C2C62-2CCB-43C9-9AF9-769AC7F22285}.Release|x86.ActiveCfg = Release|Any CPU + {0B3C2C62-2CCB-43C9-9AF9-769AC7F22285}.Release|x86.Build.0 = Release|Any CPU + {F72BAFAD-17D3-4EF2-8690-B3B5646556C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F72BAFAD-17D3-4EF2-8690-B3B5646556C2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F72BAFAD-17D3-4EF2-8690-B3B5646556C2}.Debug|x64.ActiveCfg = Debug|Any CPU + {F72BAFAD-17D3-4EF2-8690-B3B5646556C2}.Debug|x64.Build.0 = Debug|Any CPU + {F72BAFAD-17D3-4EF2-8690-B3B5646556C2}.Debug|x86.ActiveCfg = Debug|Any CPU + {F72BAFAD-17D3-4EF2-8690-B3B5646556C2}.Debug|x86.Build.0 = Debug|Any CPU + {F72BAFAD-17D3-4EF2-8690-B3B5646556C2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F72BAFAD-17D3-4EF2-8690-B3B5646556C2}.Release|Any CPU.Build.0 = Release|Any CPU + {F72BAFAD-17D3-4EF2-8690-B3B5646556C2}.Release|x64.ActiveCfg = Release|Any CPU + {F72BAFAD-17D3-4EF2-8690-B3B5646556C2}.Release|x64.Build.0 = Release|Any CPU + {F72BAFAD-17D3-4EF2-8690-B3B5646556C2}.Release|x86.ActiveCfg = Release|Any CPU + {F72BAFAD-17D3-4EF2-8690-B3B5646556C2}.Release|x86.Build.0 = Release|Any CPU {3703AF57-828D-4E2F-BFBA-A95833555A5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3703AF57-828D-4E2F-BFBA-A95833555A5A}.Debug|Any CPU.Build.0 = Debug|Any CPU {3703AF57-828D-4E2F-BFBA-A95833555A5A}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -92,10 +120,12 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution + {0B3C2C62-2CCB-43C9-9AF9-769AC7F22285} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {3703AF57-828D-4E2F-BFBA-A95833555A5A} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {0BDAE25E-C487-456D-A465-41A0CA48AF30} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {64C2B7E5-9356-457A-BAF8-F18F67DAE4CF} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {2F0041FD-FD06-4312-BF1E-180F0201EB97} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {49C19CA8-4669-4C33-B3A8-9CB934CD1171} = {07D57EEB-2F50-60C4-C011-FE4FA775C9A8} + {F72BAFAD-17D3-4EF2-8690-B3B5646556C2} = {07D57EEB-2F50-60C4-C011-FE4FA775C9A8} EndGlobalSection EndGlobal diff --git a/companion/README.md b/companion/README.md index 0f0e79f..588302c 100644 --- a/companion/README.md +++ b/companion/README.md @@ -1,27 +1,100 @@ -# Lumi Companion (experimental transcription milestone) +# Lumi Companion -This directory is the single Lumi Companion product boundary. The current milestone supplies the versioned protocol client, Windows credential protection, bounded outbound transport, plugin-process supervision, same-user OBS bridge IPC, native selected-source capture, and an Avalonia tray shell. The shell is single-instance, closes to the tray, confirms quit during OBS streaming/recording, supports explicit pairing, and reports real setup/test blockers without presenting simulated success. +Lumi Companion is the Windows streaming-computer client for Lumi. It provides a +single paired-device shell for Companion plugins, including server-hosted +transcription and Song Overlay capture. The app is single-instance, installs per +Windows user, closes to the notification area, and preserves pairing and settings +across updates. -Build prerequisites: the current .NET SDK with the .NET 8 targeting pack on Windows x64. The app targets .NET 8; .NET SDK 10 is recommended for Avalonia 12 source-generator compatibility. The native bridge additionally requires CMake, Visual Studio C++ tools, and the OBS 31+ SDK. +## Install and pair -The app accepts a `.lumi-pairing.json` bootstrap package. A paired ZIP downloaded from Lumi contains the per-user Windows setup executable and one bootstrap file. Setup installs Companion under `%LocalAppData%\Programs\Lumi Companion`, imports the adjacent package when this Windows user is not already paired, and launches the installed app. Existing portable installs retain the same current-user DPAPI credential instead of creating a duplicate device. Companion exchanges a newly imported token once and removes the pairing file after success. Do not commit bootstrap packages, credentials, generated installers, build output, or logs. +From Lumi, open **Admin > Companion** and choose **Download Companion**. The +private ZIP contains: -The companion never performs ASR in this MVP. Audio is normalized to 16 kHz mono signed 16-bit PCM, held in bounded memory, and sent to the paired Lumi host over TLS WebSockets. HTTP/WS works only for a package generated from the exact matching loopback Lumi origin. The OBS bridge talks only to the companion over a same-user named pipe. +- the checksum-pinned per-user Windows installer; +- a one-time `.lumi-pairing.json` package; +- a Host Operator notice and start instructions. -Build all managed projects from the repository root: +Extract the ZIP and run `Lumi.Companion-Setup.exe`. Setup installs to +`%LocalAppData%\Programs\Lumi Companion`, displays the licence and privacy +notices, identifies the paired Lumi host, imports the adjacent pairing package, +and opens the installed copy. The extracted download can then be deleted. + +Existing portable or installed builds retain the current Windows user's +DPAPI-protected device identity, so reinstalling does not create a duplicate +paired device. Pairing packages expire after 15 minutes, work once, and must not +be shared or committed. + +## Security boundaries + +- Companion plugins inherit the shell's paired-device authentication. Plugins do + not store or request separate Lumi credentials. +- Production Companion traffic requires HTTPS. Plain HTTP/WebSocket traffic is + accepted only when the device was paired from the exact matching loopback Lumi + origin and the request remains on loopback. +- The OBS integration communicates with Companion through a same-user named pipe. +- Transcription audio is normalized to 16 kHz mono signed 16-bit PCM, kept in + bounded memory, and sent to the paired Lumi host. Companion does not perform + local speech recognition. +- Installing or repairing the OBS integration requests elevation only while OBS + is closed and only to copy the verified managed component into OBS's shared + ProgramData plugin directory. + +## Updates + +Companion checks after connecting and every six hours. Updates are +user-approved, size- and checksum-verified, and refused while OBS is streaming or +recording. Applying an update replaces the installed executable, bundled +components, and legal bundle, then restarts Companion. Pairing credentials, +settings, and plugin state remain in the per-user data directory. + +Localhost development builds can also receive checksum-addressed same-version +updates without publishing a release. See +[Localhost development updates](../docs/local-development-updates.md). + +## Build and verify + +Requirements: + +- Windows x64; +- a current .NET SDK with the .NET 8 targeting pack; +- CMake and Visual Studio C++ tools for the native OBS bridge; +- Inno Setup 6 for the installer. + +Build the managed solution: ```powershell -dotnet build companion/Lumi.Companion.sln -p:EnableWindowsTargeting=true +dotnet build companion/Lumi.Companion.sln -c Release -p:EnableWindowsTargeting=true ``` -Build the pinned OBS 31.1.1 bridge and a self-contained Windows package from PowerShell: +Build the pinned OBS bridge, self-contained archive, legal bundle, and installer: ```powershell companion/scripts/publish-companion.ps1 ``` -This verifies both official OBS archives by SHA-256, builds the native module with Visual Studio 2022/CMake, and places the bridge beside the published app as a managed component. In Companion, **Install/Repair** requests Windows administrator approval only to copy that verified component into OBS's shared ProgramData plugin directory. OBS must be closed; Companion refuses plugin maintenance while `obs64.exe` is running. +The publish script generates the shared application/tray icon, verifies the OBS +payload inside the published executable, collects NuGet notices, and packages the +corresponding source for the GPL-licensed OBS bridge. -Experimental.3 checks for updates after connecting and every six hours. Updates remain user-approved, are checksum-verified, refuse to run while OBS is streaming or recording, replace the installed app and bundled components, then restart Companion. Pairing credentials and settings remain in the per-user data directory. Experimental.7 adds the durable per-user installer; existing portable users install it once, after which updates stay in the stable install directory. Experimental.8 fixes packaged OBS integration discovery and exposes a standalone payload diagnostic. Code signing, rollback policy, and target-machine OBS/Twitch acceptance are still required. +Run Song Overlay's focused verification with: -The Admin **Download Companion** action distributes a private ZIP containing a checksum-pinned Windows x64 installer plus the one-time pairing package. It is intentionally marked experimental and is not code-signed yet. +```powershell +companion/scripts/verify-song-overlay.ps1 +``` + +Generated installers, pairing packages, credentials, build output, and logs must +not be committed. + +## Distribution and legal notices + +The release is currently unsigned, so Windows may show an unknown-publisher +warning. The installer and installed `legal` directory include the Companion +licence, default privacy notice, third-party notices, exact licence texts, and +the corresponding OBS bridge source bundle. + +Self-hosters should set `LUMI_OPERATOR_NAME`, `LUMI_OPERATOR_CONTACT`, and +`LUMI_OPERATOR_PRIVACY_URL`. Those values identify the Host Operator responsible +for its hosted service and server-side processing; OokamiKunTV is the software +developer and is not automatically the operator of an independently hosted Lumi +installation. diff --git a/companion/docs/song-overlay-companion.md b/companion/docs/song-overlay-companion.md new file mode 100644 index 0000000..113b894 --- /dev/null +++ b/companion/docs/song-overlay-companion.md @@ -0,0 +1,38 @@ +# Song Overlay Companion integration + +This integration adds a provider-neutral media event source to Lumi Companion. Spotify is the only available provider in this release, but the Companion protocol and runtime isolate provider-specific behavior behind `IMediaProvider`. + +## Navigation contract + +Every Companion plugin implements `ICompanionPluginContribution` and receives one nested root in both navigation surfaces: + +- Tray menu: core actions, `Plugins`, one submenu per plugin, then health/update/log/settings controls and Quit. +- App sidebar: Overview, `PLUGINS`, one expandable root per plugin, then Connection, Logs, and Settings at the bottom. + +Plugin actions are caught at the shell boundary so a routine plugin exception does not terminate Companion. The Song Overlay runtime also catches provider callbacks, transport failures, heartbeats, and initialization failures and reports an actionable plugin status. + +## Song Overlay flow + +1. Windows Global System Media Transport Controls exposes Spotify playback state. +2. The provider raises media, playback, timeline, and session-availability events. +3. The runtime sends only meaningful deltas to Lumi: track change, inferred next/previous, play, resume, pause, stop, seek, metadata enrichment, or a sparse recovery heartbeat. +4. Playback progress is projected from the last event. Continuous progress messages are not sent. +5. Cover art is resized and sent only with track metadata. +6. The existing Lumi `plugins/now_playing` endpoint updates the Song Overlay source and optional chat announcement. + +## Build and verification + +From the repository root on Windows: + +```powershell +dotnet build companion/Lumi.Companion.sln -c Release -p:EnableWindowsTargeting=true +node plugins/now_playing/tests/verify.js +``` + +Or run: + +```powershell +companion/scripts/verify-song-overlay.ps1 +``` + +The optional Spotify Web API connection is used only for metadata enrichment such as exact links and release year. Core playback detection works through Windows without Spotify authorization. diff --git a/companion/installer/Lumi.Companion.iss b/companion/installer/Lumi.Companion.iss index f73d2f1..365e3ee 100644 --- a/companion/installer/Lumi.Companion.iss +++ b/companion/installer/Lumi.Companion.iss @@ -1,5 +1,5 @@ #ifndef AppVersion - #define AppVersion "0.1.0-experimental.11" + #define AppVersion "0.1.0" #endif #ifndef SourceRoot #error SourceRoot must point at the self-contained Companion publish directory. @@ -13,7 +13,8 @@ AppId={{EDEB45E4-41CB-4D7A-A479-4073A1A08B93} AppName=Lumi Companion AppVersion={#AppVersion} AppVerName=Lumi Companion {#AppVersion} -AppPublisher=Lumi +AppPublisher=OokamiKunTV +AppCopyright=Copyright (c) 2026 OokamiKunTV DefaultDirName={localappdata}\Programs\Lumi Companion DefaultGroupName=Lumi Companion DisableProgramGroupPage=yes @@ -31,24 +32,184 @@ CloseApplicationsFilter=Lumi.Companion.App.exe RestartApplications=no UninstallDisplayName=Lumi Companion UninstallDisplayIcon={app}\Lumi.Companion.App.exe +VersionInfoCompany=OokamiKunTV +VersionInfoCopyright=Copyright (c) 2026 OokamiKunTV VersionInfoDescription=Lumi Companion installer VersionInfoProductName=Lumi Companion VersionInfoProductTextVersion={#AppVersion} +LicenseFile={#SourceRoot}\legal\LUMI-COMPANION-LICENCE.txt +InfoBeforeFile={#SourceRoot}\legal\PRIVACY-NOTICE.txt +InfoAfterFile={#SourceRoot}\legal\THIRD-PARTY-NOTICES.txt [Files] Source: "{#SourceRoot}\Lumi.Companion.App.exe"; DestDir: "{app}"; Flags: ignoreversion Source: "{#SourceRoot}\components\*"; DestDir: "{app}\components"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "{#SourceRoot}\legal\*"; DestDir: "{app}\legal"; Flags: ignoreversion recursesubdirs createallsubdirs [Icons] Name: "{autoprograms}\Lumi Companion"; Filename: "{app}\Lumi.Companion.App.exe" +Name: "{autoprograms}\Lumi Companion - Legal and Privacy"; Filename: "{app}\legal\LEGAL-README.txt" [Run] Filename: "{app}\Lumi.Companion.App.exe"; Description: "Open Lumi Companion"; Flags: nowait postinstall skipifsilent [Code] -procedure ImportPairingPackage; +var + PairingPackagePath: String; + PairingHost: String; + PairingOperatorName: String; + PairingOperatorContact: String; + PairingPrivacyUrl: String; + HostAcknowledgementPage: TInputOptionWizardPage; + +function LocatePairingPackage: String; var FindRec: TFindRec; +begin + Result := ''; + if FindFirst(ExpandConstant('{src}\*.lumi-pairing.json'), FindRec) then + begin + try + Result := AddBackslash(ExpandConstant('{src}')) + FindRec.Name; + finally + FindClose(FindRec); + end; + end; +end; + +function JsonStringValue(const Json: String; const Key: String): String; +var + Marker: String; + Tail: String; + MarkerPos: Integer; + ColonPos: Integer; + EndQuotePos: Integer; +begin + Result := ''; + Marker := '"' + Key + '"'; + MarkerPos := Pos(Marker, Json); + if MarkerPos = 0 then + Exit; + + Tail := Copy(Json, MarkerPos + Length(Marker), Length(Json)); + ColonPos := Pos(':', Tail); + if ColonPos = 0 then + Exit; + + Tail := Trim(Copy(Tail, ColonPos + 1, Length(Tail))); + if (Length(Tail) < 2) or (Tail[1] <> '"') then + Exit; + + Tail := Copy(Tail, 2, Length(Tail)); + EndQuotePos := Pos('"', Tail); + if EndQuotePos = 0 then + Exit; + + Result := Copy(Tail, 1, EndQuotePos - 1); +end; + +function LoadPairingJson(const PackagePath: String; var Json: String): Boolean; +var + Lines: TArrayOfString; + Index: Integer; +begin + Result := False; + Json := ''; + if (PackagePath = '') or not LoadStringsFromFile(PackagePath, Lines) then + Exit; + + for Index := 0 to GetArrayLength(Lines) - 1 do + begin + if Index > 0 then + Json := Json + #10; + Json := Json + Lines[Index]; + end; + Result := True; +end; + +function ReadPairingHost(const Json: String): String; +var + Candidate: String; + LowerCandidate: String; +begin + Result := ''; + Candidate := JsonStringValue(Json, 'host'); + LowerCandidate := Lowercase(Candidate); + if ((Pos('https://', LowerCandidate) = 1) or (Pos('http://', LowerCandidate) = 1)) and + (Length(Candidate) <= 500) then + Result := Candidate; +end; + +procedure InitializeWizard; +var + HostDisplay: String; + Explanation: String; + OperatorDetails: String; + Json: String; +begin + PairingPackagePath := LocatePairingPackage; + if LoadPairingJson(PairingPackagePath, Json) then + begin + PairingHost := ReadPairingHost(Json); + PairingOperatorName := Copy(JsonStringValue(Json, 'operator_name'), 1, 200); + PairingOperatorContact := Copy(JsonStringValue(Json, 'operator_contact'), 1, 300); + PairingPrivacyUrl := Copy(JsonStringValue(Json, 'privacy_url'), 1, 500); + end; + + if PairingHost <> '' then + HostDisplay := PairingHost + else + HostDisplay := 'No paired Lumi host was identified beside this installer.'; + + OperatorDetails := ''; + if PairingOperatorName <> '' then + OperatorDetails := OperatorDetails + #13#10 + 'Operator name: ' + PairingOperatorName; + if PairingOperatorContact <> '' then + OperatorDetails := OperatorDetails + #13#10 + 'Operator contact: ' + PairingOperatorContact; + if PairingPrivacyUrl <> '' then + OperatorDetails := OperatorDetails + #13#10 + 'Privacy information: ' + PairingPrivacyUrl; + + Explanation := + 'This copy of Lumi Companion is intended to connect to:' + #13#10#13#10 + + HostDisplay + OperatorDetails + #13#10#13#10 + + 'The person or organisation controlling that Lumi installation is the Host Operator. ' + + 'The Host Operator is responsible for the Hosted Service it controls, including server configuration, ' + + 'access, server-side data processing, retention, integrations, and required user notices. ' + + 'OokamiKunTV is the software developer and is not automatically the operator of an independently hosted Lumi installation. ' + + 'Responsibility always follows the actual facts and applicable law.'; + + HostAcknowledgementPage := CreateInputOptionPage( + wpInfoBefore, + 'Lumi host and data responsibility', + 'Review the host that will receive Companion data.', + Explanation, + False, + False); + HostAcknowledgementPage.Add( + 'I understand which Lumi host this Companion will use and that the Host Operator controls its server-side service and processing.'); +end; + +function NextButtonClick(CurPageID: Integer): Boolean; +begin + Result := True; + { A silent deployment is an administrator/operator action and has no wizard + UI in which an interactive acknowledgement can be collected. } + if WizardSilent then + Exit; + + if (HostAcknowledgementPage <> nil) and (CurPageID = HostAcknowledgementPage.ID) and + not HostAcknowledgementPage.Values[0] then + begin + MsgBox( + 'Confirm that you understand the host and operator responsibility before continuing.', + mbInformation, + MB_OK); + Result := False; + end; +end; + +procedure ImportPairingPackage; +var SourcePath: String; TargetPath: String; begin @@ -57,17 +218,20 @@ begin the executable into its durable installed location. } if FileExists(ExpandConstant('{localappdata}\Lumi\Companion\device.credential')) then Exit; - if FindFirst(ExpandConstant('{src}\*.lumi-pairing.json'), FindRec) then - begin - try - SourcePath := AddBackslash(ExpandConstant('{src}')) + FindRec.Name; - TargetPath := AddBackslash(ExpandConstant('{app}')) + FindRec.Name; - if not CopyFile(SourcePath, TargetPath, False) then - MsgBox('Lumi Companion was installed, but its one-time pairing package could not be imported. Open Companion and choose the pairing file manually.', mbError, MB_OK); - finally - FindClose(FindRec); - end; - end; + + SourcePath := PairingPackagePath; + if SourcePath = '' then + SourcePath := LocatePairingPackage; + if SourcePath = '' then + Exit; + + TargetPath := AddBackslash(ExpandConstant('{app}')) + ExtractFileName(SourcePath); + if not CopyFile(SourcePath, TargetPath, False) then + MsgBox( + 'Lumi Companion was installed, but its one-time pairing package could not be imported. ' + + 'Open Companion and choose the pairing file manually.', + mbError, + MB_OK); end; procedure CurStepChanged(CurStep: TSetupStep); diff --git a/companion/legal/HOST-OPERATOR-NOTICE.template.txt b/companion/legal/HOST-OPERATOR-NOTICE.template.txt new file mode 100644 index 0000000..35961bd --- /dev/null +++ b/companion/legal/HOST-OPERATOR-NOTICE.template.txt @@ -0,0 +1,10 @@ +LUMI COMPANION — HOST OPERATOR NOTICE + +Paired Lumi host: {{HOST_URL}} +Operator name: {{OPERATOR_NAME}} +Operator contact: {{OPERATOR_CONTACT}} +Privacy information: {{PRIVACY_URL}} + +This package connects Companion to the Lumi installation at the host shown above. The person or organisation controlling that installation is responsible for its Hosted Service, including server configuration, user access, server-side processing, retention, integrations, notices, and compliance obligations. + +OokamiKunTV is the developer of Lumi Companion and is not automatically the operator of an independently hosted Lumi installation. Responsibility follows the actual facts and applicable law. The host URL is a technical identifier and may not by itself identify the operator’s legal name. diff --git a/companion/legal/LEGAL-MAINTAINER-NOTES.md b/companion/legal/LEGAL-MAINTAINER-NOTES.md new file mode 100644 index 0000000..7abb78c --- /dev/null +++ b/companion/legal/LEGAL-MAINTAINER-NOTES.md @@ -0,0 +1,12 @@ +# Legal maintainer notes + +This bundle is a practical baseline, not a substitute for review by a qualified lawyer familiar with the actual distribution, business model, and jurisdictions. + +Before a public or paid release: + +- publish a stable developer contact channel and, if applicable, the legal person/business name and address; +- let self-hosters configure an operator name, contact address, and privacy URL in generated pairing packages; +- confirm the project-wide licence strategy, especially the GPL-2.0-or-later OBS bridge boundary; +- retain the generated `third-party/nuget/NuGet-LICENCE-INVENTORY.txt` and copied package notices from the exact resolved .NET/NuGet dependency set for each release; +- preserve the corresponding-source archive for every distributed OBS bridge binary for as long as the relevant GPL obligations require; and +- review consumer terms, data-processing roles, and any paid-service terms before charging users. diff --git a/companion/legal/LEGAL-README.txt b/companion/legal/LEGAL-README.txt new file mode 100644 index 0000000..d6b3a17 --- /dev/null +++ b/companion/legal/LEGAL-README.txt @@ -0,0 +1,10 @@ +LUMI COMPANION — LEGAL AND PRIVACY DOCUMENTS + +- LUMI-COMPANION-LICENCE.txt: licence terms for developer-authored proprietary portions. +- PRIVACY-NOTICE.txt: default client-side data behaviour and Host Operator responsibility. +- THIRD-PARTY-NOTICES.txt: third-party components and licence locations. +- third-party directory: full third-party licence and notice files supplied with this build. +- third-party\nuget\NuGet-LICENCE-INVENTORY.txt: generated inventory and package-supplied notices for the exact restored NuGet graph. +- Lumi-OBS-Bridge-Source.zip: corresponding source and build materials for the GPL-licensed OBS bridge. + +The installer also displays the paired Lumi host URL when a pairing package is present. The person or organisation controlling that host is responsible for its own Hosted Service and server-side processing, subject to the actual facts and applicable law. diff --git a/companion/legal/LUMI-COMPANION-LICENCE.txt b/companion/legal/LUMI-COMPANION-LICENCE.txt new file mode 100644 index 0000000..ca65bd7 --- /dev/null +++ b/companion/legal/LUMI-COMPANION-LICENCE.txt @@ -0,0 +1,102 @@ +LUMI COMPANION SOFTWARE LICENCE AND TERMS OF USE +Effective date: 23 July 2026 + +PLEASE READ THESE TERMS BEFORE INSTALLING OR USING LUMI COMPANION. + +These terms govern the Lumi Companion desktop application and installer, except for components that are supplied under separate open-source or third-party licences. By installing, copying, pairing, updating, or using Lumi Companion, you accept these terms. If you do not accept them, do not install or use the software. + +1. PARTIES AND DEFINITIONS + +“Developer” means OokamiKunTV, the developer and copyright holder of Lumi Companion and the Lumi project components authored by OokamiKunTV. + +“Companion” means the Lumi Companion application, installer, updates, documentation, and developer-authored components supplied with it. + +“Host Operator” means the person or organisation that controls the Lumi installation to which Companion is paired. The paired host URL shown by the installer identifies the technical endpoint, but may not by itself identify the Host Operator’s legal name. + +“Hosted Service” means the Lumi installation, its configuration, accounts, models, storage, integrations, moderation, transcription, and other server-side functions operated by the Host Operator. + +2. SEPARATE RESPONSIBILITIES + +Companion is client software. It connects to a separately operated Lumi host selected through a pairing package or by the user. + +The Host Operator is responsible for the Hosted Service it controls, including its configuration, availability, security, access management, user notices, legal basis for processing, retention, integrations, and compliance with laws applicable to that operation. The Developer is not automatically the Host Operator merely because the Host Operator uses Lumi software. + +Where the Developer also operates the paired Lumi host, the Developer may additionally have responsibilities in that separate role. Legal responsibility always follows the actual facts and applicable law; these terms do not reassign a statutory responsibility that cannot lawfully be reassigned. + +3. LICENCE GRANT + +Subject to these terms, the Developer grants you a limited, non-exclusive, non-transferable, revocable licence to install and use Companion on devices you own or control for personal or internal organisational use with a Lumi installation you are authorised to access. + +A Host Operator may redistribute an unmodified official Companion installer together with a pairing package to users authorised to access that Host Operator’s Lumi installation. No right is granted to sell, rebrand, misrepresent, or distribute modified proprietary Companion binaries without the Developer’s permission. + +You may make reasonable backup copies. You may not remove proprietary notices, bypass access controls, use Companion to gain unauthorised access, or reverse engineer proprietary portions except to the extent that applicable law expressly permits this despite this restriction. + +Open-source components are governed only by their respective licences. Nothing in these terms limits rights granted under those licences. + +4. EXPERIMENTAL SOFTWARE AND USER RESPONSIBILITY + +Companion may be identified as experimental, preview, beta, or pre-release software. It may contain defects, change without notice, fail to interoperate with OBS, Windows, hardware, networks, third-party services, or a particular Lumi host, and may be discontinued. + +You are responsible for deciding whether Companion is suitable for your use, maintaining backups, testing changes before relying on them, protecting your accounts and devices, reviewing the Host Operator’s policies, and obtaining any permissions required before capturing or transmitting audio, captions, account information, or other data. + +Companion is not designed for emergency, medical, safety-critical, legal-compliance, life-support, or other use where failure could foreseeably cause death, personal injury, or substantial physical damage. + +5. DATA AND NETWORK FUNCTIONS + +Companion stores local settings, an installation identifier, diagnostic logs, and a Windows user-protected device credential. When paired, it communicates with the paired Lumi host and may transmit selected OBS audio, device and component status, configuration information, test data, captions, and diagnostics needed for enabled functions. + +The Privacy Notice supplied with Companion explains the default client-side behaviour. The Host Operator must provide information about server-side processing for the Hosted Service. You must not assume that the Developer receives or controls data sent to an independently operated Lumi host. + +6. UPDATES, CHANGES, AND THIRD-PARTY SERVICES + +Companion may check the paired Lumi host for updates. Installation of an update remains subject to the behaviour presented by the application. Updates may add, remove, or change features and may be required for compatibility or security. + +Companion may interoperate with third-party software and services, including OBS Studio and Windows. Those products are controlled by their respective providers and may be subject to separate terms. The Developer does not control and is not responsible for their continued availability, behaviour, policies, or changes. + +7. SUPPORT + +Unless the Developer or Host Operator separately agrees in writing, no support, maintenance, service level, response time, continued hosting, compatibility period, or update schedule is promised. + +8. NO WARRANTY + +TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, COMPANION IS PROVIDED “AS IS” AND “AS AVAILABLE”, WITH ALL FAULTS AND WITHOUT WARRANTIES, REPRESENTATIONS, CONDITIONS, OR GUARANTEES OF ANY KIND, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHERWISE. + +TO THE MAXIMUM EXTENT PERMITTED BY LAW, THE DEVELOPER DISCLAIMS IMPLIED WARRANTIES OR CONDITIONS OF MERCHANTABILITY, SATISFACTORY QUALITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, NON-INFRINGEMENT, ACCURACY, SECURITY, AVAILABILITY, COMPATIBILITY, AND ERROR-FREE OR UNINTERRUPTED OPERATION. + +No statement, documentation, test result, roadmap, or assistance creates a warranty unless the Developer expressly agrees to it in a signed written agreement. + +9. LIMITATION OF LIABILITY + +TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, THE DEVELOPER WILL NOT BE LIABLE FOR INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, PUNITIVE, OR CONSEQUENTIAL LOSS; LOSS OF DATA, CONTENT, PRIVACY, REVENUE, PROFITS, GOODWILL, BUSINESS, OPPORTUNITY, OR USE; SERVICE INTERRUPTION; DEVICE OR SOFTWARE DAMAGE; THIRD-PARTY CLAIMS; OR THE CONDUCT, SECURITY, AVAILABILITY, OR DATA PROCESSING OF AN INDEPENDENT HOST OPERATOR OR THIRD-PARTY SERVICE. + +TO THE MAXIMUM EXTENT PERMITTED BY LAW, THE DEVELOPER’S TOTAL AGGREGATE LIABILITY ARISING FROM OR RELATING TO COMPANION WILL NOT EXCEED THE AMOUNT YOU PAID DIRECTLY TO THE DEVELOPER FOR COMPANION DURING THE TWELVE MONTHS BEFORE THE EVENT GIVING RISE TO THE CLAIM. + +These exclusions and limits do not apply where liability cannot lawfully be excluded or limited, including mandatory consumer rights and, where applicable, liability for fraud, intentional misconduct, gross negligence, or death or personal injury caused by negligence. + +10. BUSINESS-USER INDEMNITY + +If you use Companion on behalf of a business or organisation, that business or organisation will, to the extent permitted by law, defend and indemnify the Developer against third-party claims, losses, and reasonable costs arising from its unlawful use of Companion, infringement of third-party rights, failure to obtain required permissions, or operation of a Lumi host in breach of law. This section does not apply to consumers acting wholly outside a trade or profession. + +11. TERMINATION + +This licence ends automatically if you materially breach these terms. On termination, you must stop using proprietary portions of Companion and remove copies under your control, except where retention is required by law or permitted by an applicable open-source licence. Sections intended by their nature to survive termination remain effective. + +12. GOVERNING LAW AND MANDATORY RIGHTS + +These terms are governed by the laws of Norway, excluding conflict-of-law rules, unless mandatory law requires otherwise. Norwegian courts have non-exclusive jurisdiction where legally permitted. + +If you are a consumer, you retain all mandatory protections and rights available under the law of your country of residence. Nothing in these terms waives a right or remedy that cannot legally be waived, restricts your right to bring proceedings in a court available under mandatory law, or requires you to accept a lower level of protection than mandatory consumer law provides. + +13. GENERAL + +If any provision is invalid or unenforceable, it will be limited or removed only to the minimum extent necessary, and the remaining provisions will continue in effect. Failure to enforce a provision is not a waiver. These terms, the Privacy Notice, and applicable third-party licences form the agreement for Companion unless a separate written agreement expressly replaces them. + +The English version controls to the extent permitted by law. A translation may be provided for convenience. + +14. CONTACT AND OPERATOR INFORMATION + +Developer: OokamiKunTV +Project: Lumi / Lumi Companion +Developer contact: use the official Lumi project support or contact channel from which the software was obtained. + +For Hosted Service, account, server-side data, moderation, retention, or access questions, contact the Host Operator identified through the paired Lumi host. The installer displays the host endpoint when a pairing package is present. diff --git a/companion/legal/PRIVACY-NOTICE.txt b/companion/legal/PRIVACY-NOTICE.txt new file mode 100644 index 0000000..9ad8109 --- /dev/null +++ b/companion/legal/PRIVACY-NOTICE.txt @@ -0,0 +1,79 @@ +LUMI COMPANION PRIVACY NOTICE +Effective date: 23 July 2026 + +This notice describes the default data behaviour of the Lumi Companion Windows application. It does not replace the privacy notice of the person or organisation operating the Lumi host to which Companion is paired. + +1. WHO IS RESPONSIBLE + +Companion is client software developed by OokamiKunTV. It can connect to Lumi installations operated by different people or organisations. + +The person or organisation operating the paired Lumi installation is normally responsible for deciding why and how server-side personal data is processed through that installation. That operator is referred to here as the “Host Operator”. The installer shows the paired host URL when it can read one from the adjacent pairing package. + +The host URL identifies a technical endpoint, not necessarily the Host Operator’s full legal identity. Obtain the operator’s identity, contact details, purposes, legal bases, recipients, retention periods, and rights information from the operator or the paired Lumi WebUI. + +OokamiKunTV is not automatically responsible for processing performed by an independently operated Lumi host. If OokamiKunTV also operates the paired host or receives information for support, security, or another stated purpose, responsibility for that processing follows the actual arrangement and applicable law. This notice does not override statutory controller or processor roles. + +2. DATA STORED ON THE WINDOWS COMPUTER + +Companion may store the following under the current Windows user profile: + +- settings, selected source identifiers, and feature preferences; +- a random installation identifier; +- a device credential containing the paired host address, device identifier, secret, capabilities, and protocol version; +- short-lived diagnostic log files containing timestamps, connection and component events, error messages, host addresses, device names, and potentially caption or test text; +- staged update files while an update is being prepared; and +- a copied one-time pairing package until it is successfully used, found redundant, manually removed, or expires. + +The device credential is protected with Windows Data Protection API for the current Windows user. This reduces casual disclosure but does not protect against every compromise of the Windows account or device. + +Diagnostic logs are pruned after seven days and capped at a combined 256 MiB by default. Uninstalling Companion may leave the per-user settings, credential, installation identifier, logs, and update directory in %LocalAppData%\Lumi\Companion so that reinstalling does not silently create a new authorised device. Use “Forget this device” before uninstalling when available, revoke the device from the Lumi host, and delete that folder manually if you want the remaining local data removed. + +3. DATA SENT TO THE PAIRED LUMI HOST + +Depending on the features you enable, Companion may send: + +- the computer or device name, installation identifier, Companion version, protocol version, capabilities, and component status; +- OBS state and selected-source information; +- audio from the OBS source you select, converted to 16 kHz mono signed 16-bit PCM and transmitted in bounded frames; +- readiness, test, benchmark, connection, and diagnostic information; and +- settings or control messages required for enabled Companion features. + +Raw audio is held in bounded memory and is not written to disk by Companion by default. Audio is transmitted to the paired Lumi host for server-side processing. The Host Operator controls the server-side speech model, storage, logs, caption delivery, integrations, and retention. Review the Host Operator’s notice before enabling capture. + +You are responsible for having authority to capture and transmit audio or other information, including informing participants and obtaining consent where required. + +4. NETWORK CONNECTIONS + +Companion connects to: + +- the host and exchange URL contained in the pairing package; +- the paired Lumi host over secure HTTP and WebSocket connections, except for an exact matching loopback host where local unencrypted transport is permitted; and +- an update artifact URL supplied by the paired Lumi host when you approve or initiate an update. + +Companion does not include advertising or general-purpose analytics telemetry in the inspected release. It does not send data directly to OokamiKunTV merely because OokamiKunTV wrote the software. An independently operated host may add integrations or server-side logging outside Companion’s control. + +5. PURPOSES AND LEGAL BASES + +Local processing is performed to pair the device, remember settings, operate enabled features, diagnose failures, verify components, and apply approved updates. + +The Host Operator determines the purposes and legal bases for server-side processing. Depending on the context, those bases may include performance of a contract, legitimate interests, consent, legal obligations, or another basis available under applicable law. Contact the Host Operator for the basis that applies to your use. + +6. RETENTION AND DELETION + +Local diagnostic logs follow the default limits described above. Credentials and settings remain until removed by the user, the application, or the operating system. Pairing tokens are designed to be single-use and expire after the period stated in the pairing package. + +Server-side retention is controlled by the Host Operator and may differ from local retention. Deleting local data does not automatically delete data already sent to the host. Revoking a device on the host does not necessarily delete prior server-side logs or content. + +7. SECURITY + +Companion validates the pairing origin, normally requires HTTPS, protects the device credential with Windows user-scoped encryption, verifies update checksums, and keeps raw audio in bounded memory by default. No security measure is perfect. Keep Windows, OBS, Companion, and the Lumi host updated; protect the Windows account; and revoke devices that are lost, shared, or no longer trusted. + +8. YOUR RIGHTS + +Depending on applicable law, you may have rights to information, access, correction, deletion, restriction, objection, portability, withdrawal of consent, and complaint to a supervisory authority. + +For data held by the paired Lumi host, direct requests to the Host Operator. For local data, use Companion controls where available, revoke the device from the host, and remove the local Companion data directory. For information actually received and controlled by OokamiKunTV, use the official Lumi project contact channel through which that processing occurred. + +9. CHANGES + +This notice may be updated when Companion’s data behaviour changes. The effective date identifies the version of the notice supplied with the installer. Material changes should be presented with a new release or through the relevant Host Operator. diff --git a/companion/legal/THIRD-PARTY-NOTICES.txt b/companion/legal/THIRD-PARTY-NOTICES.txt new file mode 100644 index 0000000..21e73d0 --- /dev/null +++ b/companion/legal/THIRD-PARTY-NOTICES.txt @@ -0,0 +1,42 @@ +LUMI COMPANION THIRD-PARTY AND OPEN-SOURCE NOTICES +Effective date: 23 July 2026 + +Lumi Companion contains or interoperates with software supplied under separate licences. Those licences govern the relevant components and take priority over the Lumi Companion Software Licence for those components. + +1. Avalonia UI +Copyright (c) AvaloniaUI OÜ. Licensed under the MIT License. +Licence file: third-party\Avalonia-MIT.txt +Project: https://github.com/AvaloniaUI/Avalonia + +2. Microsoft .NET Runtime and libraries +Copyright (c) .NET Foundation and Contributors. Licensed primarily under the MIT License and incorporating additional third-party material. +Licence files: third-party\dotnet-MIT.txt and third-party\dotnet-THIRD-PARTY-NOTICES.txt +Project: https://github.com/dotnet/runtime + +The release build also generates third-party\nuget\NuGet-LICENCE-INVENTORY.txt from the exact restored package graph and copies package-supplied licence, notice, copying, and copyright files into adjacent package directories. This covers transitive packages and native assets, including Avalonia rendering dependencies, that are present in the actual build. + +3. Inter font family +Copyright (c) The Inter Project Authors. Licensed under the SIL Open Font License, Version 1.1. +Licence file: third-party\Inter-OFL-1.1.txt +Project: https://github.com/rsms/inter + +4. nlohmann/json 3.12.0 +Copyright (c) 2013-2025 Niels Lohmann. Licensed primarily under the MIT License. The upstream project also identifies incorporated material under compatible licences, including Apache-2.0 and public-domain/CC0 material. +Licence files: third-party\nlohmann-json-MIT.txt and third-party\Apache-2.0.txt +Project: https://github.com/nlohmann/json + +5. OBS Studio / libobs and Lumi OBS Bridge +OBS Studio is distributed under the GNU General Public License, version 2 or any later version. Lumi Companion does not distribute OBS Studio itself, but the bundled Lumi OBS Bridge links with OBS/libobs and is distributed under GPL-2.0-or-later. +Licence file: third-party\GPL-2.0-or-later.txt +Corresponding source: Lumi-OBS-Bridge-Source.zip installed in the legal directory. +OBS project: https://github.com/obsproject/obs-studio + +6. Inno Setup +The Windows installer was created with Inno Setup by Jordan Russell and Martijn Laan. Inno Setup is supplied under its own licence. +Licence file: third-party\Inno-Setup-License.txt +Project: https://jrsoftware.org/isinfo.php + +7. Windows and third-party products +Microsoft Windows, Windows Data Protection API, OBS Studio, and other named products are trademarks or products of their respective owners. Their inclusion or mention does not imply sponsorship or endorsement. + +The source archive and licence files are provided to preserve the rights granted by the applicable open-source licences. If a required notice is believed to be missing, report it through the official Lumi project contact channel so it can be corrected. diff --git a/companion/legal/third-party/Apache-2.0.txt b/companion/legal/third-party/Apache-2.0.txt new file mode 100644 index 0000000..137069b --- /dev/null +++ b/companion/legal/third-party/Apache-2.0.txt @@ -0,0 +1,73 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/companion/legal/third-party/Avalonia-MIT.txt b/companion/legal/third-party/Avalonia-MIT.txt new file mode 100644 index 0000000..4195180 --- /dev/null +++ b/companion/legal/third-party/Avalonia-MIT.txt @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) AvaloniaUI OÜ +All Rights Reserved + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/companion/legal/third-party/GPL-2.0-or-later.txt b/companion/legal/third-party/GPL-2.0-or-later.txt new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/companion/legal/third-party/GPL-2.0-or-later.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/companion/legal/third-party/Inno-Setup-License.txt b/companion/legal/third-party/Inno-Setup-License.txt new file mode 100644 index 0000000..3f3deb3 --- /dev/null +++ b/companion/legal/third-party/Inno-Setup-License.txt @@ -0,0 +1,32 @@ +Inno Setup License +================== + +Except where otherwise noted, all of the documentation and software included in the Inno +Setup package is copyrighted by Jordan Russell. + +Copyright (C) 1997-2026 Jordan Russell. All rights reserved. +Portions Copyright (C) 2000-2026 Martijn Laan. All rights reserved. + +This software is provided "as-is," without any express or implied warranty. In no event shall +the author be held liable for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including commercial +applications, and to alter and redistribute it, provided that the following conditions are met: + +1. All redistributions of source code files must retain all copyright notices that are currently + in place, and this list of conditions without modification. + +2. All redistributions in binary form must retain all occurrences of the above copyright notice + and web site addresses that are currently in place (for example, in the About boxes). + +3. The origin of this software must not be misrepresented; you must not claim that you wrote + the original software. If you use this software to distribute a product, an acknowledgment + in the product documentation would be appreciated but is not required. + +4. Modified versions in source or binary form must be plainly marked as such, and must not + be misrepresented as being the original software. + + +Jordan Russell +jr-2020 AT jrsoftware.org +https://jrsoftware.org/ \ No newline at end of file diff --git a/companion/legal/third-party/Inter-OFL-1.1.txt b/companion/legal/third-party/Inter-OFL-1.1.txt new file mode 100644 index 0000000..9b2ca37 --- /dev/null +++ b/companion/legal/third-party/Inter-OFL-1.1.txt @@ -0,0 +1,92 @@ +Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION AND CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/companion/legal/third-party/dotnet-MIT.txt b/companion/legal/third-party/dotnet-MIT.txt new file mode 100644 index 0000000..984713a --- /dev/null +++ b/companion/legal/third-party/dotnet-MIT.txt @@ -0,0 +1,23 @@ +The MIT License (MIT) + +Copyright (c) .NET Foundation and Contributors + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/companion/legal/third-party/dotnet-THIRD-PARTY-NOTICES.txt b/companion/legal/third-party/dotnet-THIRD-PARTY-NOTICES.txt new file mode 100644 index 0000000..9b4e777 --- /dev/null +++ b/companion/legal/third-party/dotnet-THIRD-PARTY-NOTICES.txt @@ -0,0 +1,1272 @@ +.NET Runtime uses third-party libraries or other resources that may be +distributed under licenses different than the .NET Runtime software. + +In the event that we accidentally failed to list a required notice, please +bring it to our attention. Post an issue or email us: + + dotnet@microsoft.com + +The attached notices are provided for information only. + +License notice for ASP.NET +------------------------------- + +Copyright (c) .NET Foundation. All rights reserved. +Licensed under the Apache License, Version 2.0. + +Available at +https://github.com/dotnet/aspnetcore/blob/main/LICENSE.txt + +License notice for Slicing-by-8 +------------------------------- + +http://sourceforge.net/projects/slicing-by-8/ + +Copyright (c) 2004-2006 Intel Corporation - All Rights Reserved + + +This software program is licensed subject to the BSD License, available at +http://www.opensource.org/licenses/bsd-license.html. + + +License notice for Unicode data +------------------------------- + +https://www.unicode.org/license.html + +Copyright © 1991-2022 Unicode, Inc. All rights reserved. +Distributed under the Terms of Use in https://www.unicode.org/copyright.html. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Unicode data files and any associated documentation +(the "Data Files") or Unicode software and any associated documentation +(the "Software") to deal in the Data Files or Software +without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, and/or sell copies of +the Data Files or Software, and to permit persons to whom the Data Files +or Software are furnished to do so, provided that either +(a) this copyright and permission notice appear with all copies +of the Data Files or Software, or +(b) this copyright and permission notice appear in associated +Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT OF THIRD PARTY RIGHTS. +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS +NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL +DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THE DATA FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder +shall not be used in advertising or otherwise to promote the sale, +use or other dealings in these Data Files or Software without prior +written authorization of the copyright holder. + +License notice for Zlib +----------------------- + +https://github.com/madler/zlib +https://zlib.net/zlib_license.html + +/* zlib.h -- interface of the 'zlib' general purpose compression library + version 1.3.1, January 22nd, 2024 + + Copyright (C) 1995-2022 Jean-loup Gailly and Mark Adler + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + Jean-loup Gailly Mark Adler + jloup@gzip.org madler@alumni.caltech.edu + +*/ + +License notice for Mono +------------------------------- + +http://www.mono-project.com/docs/about-mono/ + +Copyright (c) .NET Foundation Contributors + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the Software), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for International Organization for Standardization +----------------------------------------------------------------- + +Portions (C) International Organization for Standardization 1986: + Permission to copy in any form is granted for use with + conforming SGML systems and applications as defined in + ISO 8879, provided this notice is included in all copies. + +License notice for Intel +------------------------ + +"Copyright (c) 2004-2006 Intel Corporation - All Rights Reserved + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for Xamarin and Novell +------------------------------------- + +Copyright (c) 2015 Xamarin, Inc (http://www.xamarin.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +Copyright (c) 2011 Novell, Inc (http://www.novell.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +Third party notice for W3C +-------------------------- + +"W3C SOFTWARE AND DOCUMENT NOTICE AND LICENSE +Status: This license takes effect 13 May, 2015. +This work is being provided by the copyright holders under the following license. +License +By obtaining and/or copying this work, you (the licensee) agree that you have read, understood, and will comply with the following terms and conditions. +Permission to copy, modify, and distribute this work, with or without modification, for any purpose and without fee or royalty is hereby granted, provided that you include the following on ALL copies of the work or portions thereof, including modifications: +The full text of this NOTICE in a location viewable to users of the redistributed or derivative work. +Any pre-existing intellectual property disclaimers, notices, or terms and conditions. If none exist, the W3C Software and Document Short Notice should be included. +Notice of any changes or modifications, through a copyright statement on the new code or document such as "This software or document includes material copied from or derived from [title and URI of the W3C document]. Copyright © [YEAR] W3C® (MIT, ERCIM, Keio, Beihang)." +Disclaimers +THIS WORK IS PROVIDED "AS IS," AND COPYRIGHT HOLDERS MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE OR DOCUMENT WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS. +COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR DOCUMENT. +The name and trademarks of copyright holders may NOT be used in advertising or publicity pertaining to the work without specific, written prior permission. Title to copyright in this work will at all times remain with copyright holders." + +License notice for Bit Twiddling Hacks +-------------------------------------- + +Bit Twiddling Hacks + +By Sean Eron Anderson +seander@cs.stanford.edu + +Individually, the code snippets here are in the public domain (unless otherwise +noted) — feel free to use them however you please. The aggregate collection and +descriptions are © 1997-2005 Sean Eron Anderson. The code and descriptions are +distributed in the hope that they will be useful, but WITHOUT ANY WARRANTY and +without even the implied warranty of merchantability or fitness for a particular +purpose. + +License notice for Brotli +-------------------------------------- + +Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +compress_fragment.c: +Copyright (c) 2011, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +""AS IS"" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +decode_fuzzer.c: +Copyright (c) 2015 The Chromium Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +""AS IS"" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." + +License notice for Json.NET +------------------------------- + +https://github.com/JamesNK/Newtonsoft.Json/blob/master/LICENSE.md + +The MIT License (MIT) + +Copyright (c) 2007 James Newton-King + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for vectorized base64 encoding / decoding +-------------------------------------------------------- + +Copyright (c) 2005-2007, Nick Galbreath +Copyright (c) 2013-2017, Alfred Klomp +Copyright (c) 2015-2017, Wojciech Mula +Copyright (c) 2016-2017, Matthieu Darbois +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for vectorized hex parsing +-------------------------------------------------------- + +Copyright (c) 2022, Geoff Langdale +Copyright (c) 2022, Wojciech Mula +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for RFC 3492 +--------------------------- + +The punycode implementation is based on the sample code in RFC 3492 + +Copyright (C) The Internet Society (2003). All Rights Reserved. + +This document and translations of it may be copied and furnished to +others, and derivative works that comment on or otherwise explain it +or assist in its implementation may be prepared, copied, published +and distributed, in whole or in part, without restriction of any +kind, provided that the above copyright notice and this paragraph are +included on all such copies and derivative works. However, this +document itself may not be modified in any way, such as by removing +the copyright notice or references to the Internet Society or other +Internet organizations, except as needed for the purpose of +developing Internet standards in which case the procedures for +copyrights defined in the Internet Standards process must be +followed, or as required to translate it into languages other than +English. + +The limited permissions granted above are perpetual and will not be +revoked by the Internet Society or its successors or assigns. + +This document and the information contained herein is provided on an +"AS IS" basis and THE INTERNET SOCIETY AND THE INTERNET ENGINEERING +TASK FORCE DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING +BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE INFORMATION +HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED WARRANTIES OF +MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + +Copyright(C) The Internet Society 1997. All Rights Reserved. + +This document and translations of it may be copied and furnished to others, +and derivative works that comment on or otherwise explain it or assist in +its implementation may be prepared, copied, published and distributed, in +whole or in part, without restriction of any kind, provided that the above +copyright notice and this paragraph are included on all such copies and +derivative works.However, this document itself may not be modified in any +way, such as by removing the copyright notice or references to the Internet +Society or other Internet organizations, except as needed for the purpose of +developing Internet standards in which case the procedures for copyrights +defined in the Internet Standards process must be followed, or as required +to translate it into languages other than English. + +The limited permissions granted above are perpetual and will not be revoked +by the Internet Society or its successors or assigns. + +This document and the information contained herein is provided on an "AS IS" +basis and THE INTERNET SOCIETY AND THE INTERNET ENGINEERING TASK FORCE +DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +ANY WARRANTY THAT THE USE OF THE INFORMATION HEREIN WILL NOT INFRINGE ANY +RIGHTS OR ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A +PARTICULAR PURPOSE. + +License notice for Algorithm from RFC 4122 - +A Universally Unique IDentifier (UUID) URN Namespace +---------------------------------------------------- + +Copyright (c) 1990- 1993, 1996 Open Software Foundation, Inc. +Copyright (c) 1989 by Hewlett-Packard Company, Palo Alto, Ca. & +Digital Equipment Corporation, Maynard, Mass. +Copyright (c) 1998 Microsoft. +To anyone who acknowledges that this file is provided "AS IS" +without any express or implied warranty: permission to use, copy, +modify, and distribute this file for any purpose is hereby +granted without fee, provided that the above copyright notices and +this notice appears in all source code copies, and that none of +the names of Open Software Foundation, Inc., Hewlett-Packard +Company, Microsoft, or Digital Equipment Corporation be used in +advertising or publicity pertaining to distribution of the software +without specific, written prior permission. Neither Open Software +Foundation, Inc., Hewlett-Packard Company, Microsoft, nor Digital +Equipment Corporation makes any representations about the +suitability of this software for any purpose." + +License notice for The LLVM Compiler Infrastructure (Legacy License) +-------------------------------------------------------------------- + +Developed by: + + LLVM Team + + University of Illinois at Urbana-Champaign + + http://llvm.org + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal with +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimers. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimers in the + documentation and/or other materials provided with the distribution. + + * Neither the names of the LLVM Team, University of Illinois at + Urbana-Champaign, nor the names of its contributors may be used to + endorse or promote products derived from this Software without specific + prior written permission. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE +SOFTWARE. + +License notice for Bob Jenkins +------------------------------ + +By Bob Jenkins, 1996. bob_jenkins@burtleburtle.net. You may use this +code any way you wish, private, educational, or commercial. It's free. + +License notice for Greg Parker +------------------------------ + +Greg Parker gparker@cs.stanford.edu December 2000 +This code is in the public domain and may be copied or modified without +permission. + +License notice for libunwind based code +---------------------------------------- + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for Printing Floating-Point Numbers (Dragon4) +------------------------------------------------------------ + +/****************************************************************************** + Copyright (c) 2014 Ryan Juckett + http://www.ryanjuckett.com/ + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source + distribution. +******************************************************************************/ + +License notice for Printing Floating-point Numbers (Grisu3) +----------------------------------------------------------- + +Copyright 2012 the V8 project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for xxHash +------------------------- + +xxHash - Extremely Fast Hash algorithm +Header File +Copyright (C) 2012-2021 Yann Collet + +BSD 2-Clause License (https://www.opensource.org/licenses/bsd-license.php) + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with the + distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +You can contact the author at: + - xxHash homepage: https://www.xxhash.com + - xxHash source repository: https://github.com/Cyan4973/xxHash + +License notice for Berkeley SoftFloat Release 3e +------------------------------------------------ + +https://github.com/ucb-bar/berkeley-softfloat-3 +https://github.com/ucb-bar/berkeley-softfloat-3/blob/master/COPYING.txt + +License for Berkeley SoftFloat Release 3e + +John R. Hauser +2018 January 20 + +The following applies to the whole of SoftFloat Release 3e as well as to +each source file individually. + +Copyright 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018 The Regents of the +University of California. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions, and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions, and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of the University nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS "AS IS", AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ARE +DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for xoshiro RNGs +-------------------------------- + +Written in 2018 by David Blackman and Sebastiano Vigna (vigna@acm.org) + +To the extent possible under law, the author has dedicated all copyright +and related and neighboring rights to this software to the public domain +worldwide. This software is distributed without any warranty. + +See . + +License for fastmod (https://github.com/lemire/fastmod), ibm-fpgen (https://github.com/nigeltao/parse-number-fxx-test-data) and fastrange (https://github.com/lemire/fastrange) +-------------------------------------- + + Copyright 2018 Daniel Lemire + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +License for sse4-strstr (https://github.com/WojciechMula/sse4-strstr) +-------------------------------------- + + Copyright (c) 2008-2016, Wojciech Mula + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for The C++ REST SDK +----------------------------------- + +C++ REST SDK + +The MIT License (MIT) + +Copyright (c) Microsoft Corporation + +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for MessagePack-CSharp +------------------------------------- + +MessagePack for C# + +MIT License + +Copyright (c) 2017 Yoshifumi Kawai + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for lz4net +------------------------------------- + +lz4net + +Copyright (c) 2013-2017, Milosz Krajewski + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for Nerdbank.Streams +----------------------------------- + +The MIT License (MIT) + +Copyright (c) Andrew Arnott + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for RapidJSON +---------------------------- + +Tencent is pleased to support the open source community by making RapidJSON available. + +Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. All rights reserved. + +Licensed under the MIT License (the "License"); you may not use this file except +in compliance with the License. You may obtain a copy of the License at + +http://opensource.org/licenses/MIT + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. + +License notice for DirectX Math Library +--------------------------------------- + +https://github.com/microsoft/DirectXMath/blob/master/LICENSE + + The MIT License (MIT) + +Copyright (c) 2011-2020 Microsoft Corp + +Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be included in all copies +or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for ldap4net +--------------------------- + +The MIT License (MIT) + +Copyright (c) 2018 Alexander Chermyanin + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for vectorized sorting code +------------------------------------------ + +MIT License + +Copyright (c) 2020 Dan Shechter + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for musl +----------------------- + +musl as a whole is licensed under the following standard MIT license: + +Copyright © 2005-2020 Rich Felker, et al. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +License notice for "Faster Unsigned Division by Constants" +------------------------------ + +Reference implementations of computing and using the "magic number" approach to dividing +by constants, including codegen instructions. The unsigned division incorporates the +"round down" optimization per ridiculous_fish. + +This is free and unencumbered software. Any copyright is dedicated to the Public Domain. + + +License notice for mimalloc +----------------------------------- + +MIT License + +Copyright (c) 2019 Microsoft Corporation, Daan Leijen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for The LLVM Project +----------------------------------- + +Copyright 2019 LLVM Project + +Licensed under the Apache License, Version 2.0 (the "License") with LLVM Exceptions; +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +https://llvm.org/LICENSE.txt + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +License notice for Apple header files +------------------------------------- + +Copyright (c) 1980, 1986, 1993 + The Regents of the University of California. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. All advertising materials mentioning features or use of this software + must display the following acknowledgement: + This product includes software developed by the University of + California, Berkeley and its contributors. +4. Neither the name of the University nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. + +License notice for JavaScript queues +------------------------------------- + +CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER. + +Statement of Purpose +The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). +Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. +For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: +the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; +moral rights retained by the original author(s) and/or performer(s); +publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; +rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; +rights protecting the extraction, dissemination, use and reuse of data in a Work; +database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and +other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. +2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. +3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. +4. Limitations and Disclaimers. +a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. +b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. +c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. +d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. + + +License notice for FastFloat algorithm +------------------------------------- +MIT License +Copyright (c) 2021 csFastFloat authors +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for MsQuic +-------------------------------------- + +Copyright (c) Microsoft Corporation. +Licensed under the MIT License. + +Available at +https://github.com/microsoft/msquic/blob/main/LICENSE + +License notice for m-ou-se/floatconv +------------------------------- + +Copyright (c) 2020 Mara Bos +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License notice for code from The Practice of Programming +------------------------------- + +Copyright (C) 1999 Lucent Technologies + +Excerpted from 'The Practice of Programming +by Brian W. Kernighan and Rob Pike + +You may use this code for any purpose, as long as you leave the copyright notice and book citation attached. + +Notice for Euclidean Affine Functions and Applications to Calendar +Algorithms +------------------------------- + +Aspects of Date/Time processing based on algorithm described in "Euclidean Affine Functions and Applications to Calendar +Algorithms", Cassio Neri and Lorenz Schneider. https://arxiv.org/pdf/2102.06959.pdf + +License notice for amd/aocl-libm-ose +------------------------------- + +Copyright (C) 2008-2020 Advanced Micro Devices, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +License notice for fmtlib/fmt +------------------------------- + +Formatting library for C++ + +Copyright (c) 2012 - present, Victor Zverovich + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License for Jb Evain +--------------------- + +Copyright (c) 2006 Jb Evain (jbevain@gmail.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +--- Optional exception to the license --- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into a machine-executable object form of such +source code, you may redistribute such embedded portions in such object form +without including the above copyright and permission notices. + + +License for MurmurHash3 +-------------------------------------- + +https://github.com/aappleby/smhasher/blob/master/src/MurmurHash3.cpp + +MurmurHash3 was written by Austin Appleby, and is placed in the public +domain. The author hereby disclaims copyright to this source + +License for Fast CRC Computation +-------------------------------------- + +https://github.com/intel/isa-l/blob/33a2d9484595c2d6516c920ce39a694c144ddf69/crc/crc32_ieee_by4.asm +https://github.com/intel/isa-l/blob/33a2d9484595c2d6516c920ce39a694c144ddf69/crc/crc64_ecma_norm_by8.asm + +Copyright(c) 2011-2015 Intel Corporation All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of Intel Corporation nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +License for C# Implementation of Fast CRC Computation +----------------------------------------------------- + +https://github.com/SixLabors/ImageSharp/blob/f4f689ce67ecbcc35cebddba5aacb603e6d1068a/src/ImageSharp/Formats/Png/Zlib/Crc32.cs + +Copyright (c) Six Labors. +Licensed under the Apache License, Version 2.0. + +Available at +https://github.com/SixLabors/ImageSharp/blob/f4f689ce67ecbcc35cebddba5aacb603e6d1068a/LICENSE diff --git a/companion/legal/third-party/nlohmann-json-MIT.txt b/companion/legal/third-party/nlohmann-json-MIT.txt new file mode 100644 index 0000000..b5a1027 --- /dev/null +++ b/companion/legal/third-party/nlohmann-json-MIT.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2013-2025 Niels Lohmann + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/companion/native/obs-bridge/CMakeLists.txt b/companion/native/obs-bridge/CMakeLists.txt index 0894baf..8107c8f 100644 --- a/companion/native/obs-bridge/CMakeLists.txt +++ b/companion/native/obs-bridge/CMakeLists.txt @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Copyright (c) 2026 OokamiKunTV + cmake_minimum_required(VERSION 3.28) project(lumi-obs-bridge VERSION 0.1.0 LANGUAGES CXX) set(LUMI_BRIDGE_VERSION "0.1.0-development" CACHE STRING "Lumi Companion bridge release version") diff --git a/companion/native/obs-bridge/COPYING b/companion/native/obs-bridge/COPYING new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/companion/native/obs-bridge/COPYING @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/companion/native/obs-bridge/README.md b/companion/native/obs-bridge/README.md new file mode 100644 index 0000000..e3a6569 --- /dev/null +++ b/companion/native/obs-bridge/README.md @@ -0,0 +1,9 @@ +# Lumi OBS Bridge + +The Lumi OBS Bridge is the native OBS module bundled with Lumi Companion. + +Copyright (c) 2026 OokamiKunTV. + +This component is free software licensed under the GNU General Public License, version 2 or (at your option) any later version (`GPL-2.0-or-later`). See `COPYING` for the complete licence. + +The release build includes a corresponding-source archive containing this directory and the build script used to produce the distributed binary. OBS Studio and nlohmann/json remain available from their upstream projects under their respective licences. diff --git a/companion/native/obs-bridge/include/bounded_spsc_queue.hpp b/companion/native/obs-bridge/include/bounded_spsc_queue.hpp index 7afb47b..a72345b 100644 --- a/companion/native/obs-bridge/include/bounded_spsc_queue.hpp +++ b/companion/native/obs-bridge/include/bounded_spsc_queue.hpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (c) 2026 OokamiKunTV + #pragma once #include #include diff --git a/companion/native/obs-bridge/src/plugin.cpp b/companion/native/obs-bridge/src/plugin.cpp index c4455db..eef70cb 100644 --- a/companion/native/obs-bridge/src/plugin.cpp +++ b/companion/native/obs-bridge/src/plugin.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (c) 2026 OokamiKunTV + #include #include #include diff --git a/companion/plugins/Lumi.Companion.SongOverlay/Lumi.Companion.SongOverlay.csproj b/companion/plugins/Lumi.Companion.SongOverlay/Lumi.Companion.SongOverlay.csproj new file mode 100644 index 0000000..c983fc2 --- /dev/null +++ b/companion/plugins/Lumi.Companion.SongOverlay/Lumi.Companion.SongOverlay.csproj @@ -0,0 +1,15 @@ + + + net8.0-windows10.0.19041.0 + enable + enable + true + Lumi.Companion.SongOverlay + Lumi.Companion.SongOverlay + + + + + + + diff --git a/companion/plugins/Lumi.Companion.SongOverlay/MediaModels.cs b/companion/plugins/Lumi.Companion.SongOverlay/MediaModels.cs new file mode 100644 index 0000000..ece43fb --- /dev/null +++ b/companion/plugins/Lumi.Companion.SongOverlay/MediaModels.cs @@ -0,0 +1,53 @@ +using System.Text.Json.Serialization; + +namespace Lumi.Companion.SongOverlay; + +internal sealed record MediaTrack( + string Key, + string Title, + string Artist, + string Album, + string ReleaseYear, + string Link, + long DurationMilliseconds, + CoverPayload? Cover); + +internal sealed record MediaSnapshot( + string Provider, + string Status, + long PositionMilliseconds, + long DurationMilliseconds, + double PlaybackRate, + MediaTrack? Track, + DateTimeOffset CapturedAt); + +internal sealed record CoverPayload( + [property: JsonPropertyName("mime")] string Mime, + [property: JsonPropertyName("base64")] string Base64, + [property: JsonPropertyName("primary")] string Primary, + [property: JsonPropertyName("secondary")] string Secondary); + +internal sealed record TrackPayload( + [property: JsonPropertyName("key")] string Key, + [property: JsonPropertyName("title")] string Title, + [property: JsonPropertyName("artist")] string Artist, + [property: JsonPropertyName("album")] string Album, + [property: JsonPropertyName("release_year")] string ReleaseYear, + [property: JsonPropertyName("link")] string Link, + [property: JsonPropertyName("cover")] CoverPayload? Cover); + +internal sealed record PlaybackPayload( + [property: JsonPropertyName("status")] string Status, + [property: JsonPropertyName("position_ms")] long PositionMilliseconds, + [property: JsonPropertyName("duration_ms")] long DurationMilliseconds, + [property: JsonPropertyName("rate")] double Rate); + +internal sealed record NowPlayingEventPayload( + [property: JsonPropertyName("protocol_version")] int ProtocolVersion, + [property: JsonPropertyName("provider")] string Provider, + [property: JsonPropertyName("session_id")] string SessionId, + [property: JsonPropertyName("sequence")] long Sequence, + [property: JsonPropertyName("event")] string Event, + [property: JsonPropertyName("occurred_at")] long OccurredAt, + [property: JsonPropertyName("playback")] PlaybackPayload Playback, + [property: JsonPropertyName("track")] TrackPayload? Track); diff --git a/companion/plugins/Lumi.Companion.SongOverlay/Providers/IMediaProvider.cs b/companion/plugins/Lumi.Companion.SongOverlay/Providers/IMediaProvider.cs new file mode 100644 index 0000000..e937aa0 --- /dev/null +++ b/companion/plugins/Lumi.Companion.SongOverlay/Providers/IMediaProvider.cs @@ -0,0 +1,24 @@ +namespace Lumi.Companion.SongOverlay.Providers; + +internal interface IMediaProvider : IAsyncDisposable +{ + string Id { get; } + string DisplayName { get; } + event EventHandler? StateChanged; + event EventHandler? AvailabilityChanged; + Task StartAsync(CancellationToken cancellationToken); + Task StopAsync(CancellationToken cancellationToken); + Task GetSnapshotAsync(CancellationToken cancellationToken); +} + +internal sealed class ProviderStateChangedEventArgs : EventArgs +{ + public ProviderStateChangedEventArgs(string reason, MediaSnapshot? snapshot) + { + Reason = reason; + Snapshot = snapshot; + } + + public string Reason { get; } + public MediaSnapshot? Snapshot { get; } +} diff --git a/companion/plugins/Lumi.Companion.SongOverlay/Providers/SpotifyWindowsMediaProvider.cs b/companion/plugins/Lumi.Companion.SongOverlay/Providers/SpotifyWindowsMediaProvider.cs new file mode 100644 index 0000000..c1165ed --- /dev/null +++ b/companion/plugins/Lumi.Companion.SongOverlay/Providers/SpotifyWindowsMediaProvider.cs @@ -0,0 +1,322 @@ +using System.Drawing; +using System.Drawing.Imaging; +using System.Security.Cryptography; +using System.Text; +using Windows.Media.Control; +using Windows.Storage.Streams; + +namespace Lumi.Companion.SongOverlay.Providers; + +internal sealed class SpotifyWindowsMediaProvider : IMediaProvider +{ + private const int MaxCoverInputBytes = 3 * 1024 * 1024; + private const int MaxCoverOutputBytes = 500 * 1024; + private readonly Func _settings; + private readonly Func> _enrich; + private readonly Action _log; + private GlobalSystemMediaTransportControlsSessionManager? _manager; + private GlobalSystemMediaTransportControlsSession? _session; + private MediaSnapshot? _last; + private MediaTrack? _trackCache; + private bool _started; + private readonly SemaphoreSlim _refreshLock = new(1, 1); + + public SpotifyWindowsMediaProvider(Func settings, Func> enrich, Action log) + { + _settings = settings; + _enrich = enrich; + _log = log; + } + + public string Id => "spotify"; + public string DisplayName => "Spotify"; + public event EventHandler? StateChanged; + public event EventHandler? AvailabilityChanged; + + public async Task StartAsync(CancellationToken cancellationToken) + { + if (_started) return; + _manager = await GlobalSystemMediaTransportControlsSessionManager.RequestAsync(); + _manager.CurrentSessionChanged += OnSessionCollectionChanged; + _manager.SessionsChanged += OnSessionCollectionChanged; + _started = true; + await SelectSessionAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + if (!_started) return; + if (_manager is not null) + { + _manager.CurrentSessionChanged -= OnSessionCollectionChanged; + _manager.SessionsChanged -= OnSessionCollectionChanged; + } + DetachSession(); + _manager = null; + _started = false; + await Task.CompletedTask; + } + + public Task GetSnapshotAsync(CancellationToken cancellationToken) => RefreshAsync("snapshot", true, cancellationToken); + + private async void OnSessionCollectionChanged(GlobalSystemMediaTransportControlsSessionManager sender, object args) + { + try { await SelectSessionAsync(CancellationToken.None).ConfigureAwait(false); } + catch (Exception error) { _log("Could not refresh Spotify media sessions", error); } + } + + private async Task SelectSessionAsync(CancellationToken cancellationToken) + { + var selected = _manager?.GetSessions().FirstOrDefault(IsSpotifySession); + if (ReferenceEquals(selected, _session)) + { + if (_session is not null) await RefreshAndPublishAsync("snapshot", false, cancellationToken).ConfigureAwait(false); + return; + } + DetachSession(); + _session = selected; + if (_session is null) + { + AvailabilityChanged?.Invoke(this, "Spotify is not exposing a Windows media session."); + if (_last is not null) + { + _last = null; + StateChanged?.Invoke(this, new ProviderStateChangedEventArgs("stop", null)); + } + return; + } + _session.MediaPropertiesChanged += OnMediaPropertiesChanged; + _session.PlaybackInfoChanged += OnPlaybackInfoChanged; + _session.TimelinePropertiesChanged += OnTimelinePropertiesChanged; + AvailabilityChanged?.Invoke(this, "Spotify media session connected."); + await RefreshAndPublishAsync("snapshot", true, cancellationToken).ConfigureAwait(false); + } + + private void DetachSession() + { + if (_session is null) return; + _session.MediaPropertiesChanged -= OnMediaPropertiesChanged; + _session.PlaybackInfoChanged -= OnPlaybackInfoChanged; + _session.TimelinePropertiesChanged -= OnTimelinePropertiesChanged; + _session = null; + _trackCache = null; + } + + private async void OnMediaPropertiesChanged(GlobalSystemMediaTransportControlsSession sender, MediaPropertiesChangedEventArgs args) + { + try { await RefreshAndPublishAsync("media", true, CancellationToken.None).ConfigureAwait(false); } + catch (Exception error) { _log("Spotify media-property update failed", error); } + } + + private async void OnPlaybackInfoChanged(GlobalSystemMediaTransportControlsSession sender, PlaybackInfoChangedEventArgs args) + { + try { await RefreshAndPublishAsync("playback", false, CancellationToken.None).ConfigureAwait(false); } + catch (Exception error) { _log("Spotify playback update failed", error); } + } + + private async void OnTimelinePropertiesChanged(GlobalSystemMediaTransportControlsSession sender, TimelinePropertiesChangedEventArgs args) + { + try { await RefreshAndPublishAsync("timeline", false, CancellationToken.None).ConfigureAwait(false); } + catch (Exception error) { _log("Spotify timeline update failed", error); } + } + + private async Task RefreshAndPublishAsync(string reason, bool enrichTrack, CancellationToken cancellationToken) + { + var snapshot = await RefreshAsync(reason, enrichTrack, cancellationToken).ConfigureAwait(false); + if (snapshot is null) return; + StateChanged?.Invoke(this, new ProviderStateChangedEventArgs(reason, snapshot)); + } + + private async Task RefreshAsync(string reason, bool enrichTrack, CancellationToken cancellationToken) + { + if (_session is null) return null; + await _refreshLock.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + var playback = _session.GetPlaybackInfo(); + var timeline = _session.GetTimelineProperties(); + var duration = Math.Max(0, (long)timeline.EndTime.TotalMilliseconds); + var position = Math.Max(0, (long)timeline.Position.TotalMilliseconds); + var status = playback.PlaybackStatus switch + { + GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing => "playing", + GlobalSystemMediaTransportControlsSessionPlaybackStatus.Paused => "paused", + GlobalSystemMediaTransportControlsSessionPlaybackStatus.Stopped => "stopped", + GlobalSystemMediaTransportControlsSessionPlaybackStatus.Closed => "closed", + _ => "unknown" + }; + var capturedAt = DateTimeOffset.UtcNow; + var rate = playback.PlaybackRate ?? 1d; + if (rate <= 0) rate = 1d; + if (status == "playing" && timeline.LastUpdatedTime != default) + { + var elapsed = Math.Max(0, (capturedAt - timeline.LastUpdatedTime).TotalMilliseconds); + position += (long)(elapsed * rate); + if (duration > 0) position = Math.Min(duration, position); + } + + var needsMediaRefresh = _trackCache is null || reason is "media" or "snapshot"; + if (needsMediaRefresh) + { + var media = await _session.TryGetMediaPropertiesAsync(); + var title = Clean(media.Title); + var artist = Clean(media.Artist); + var album = Clean(media.AlbumTitle); + if (!string.IsNullOrWhiteSpace(title) || !string.IsNullOrWhiteSpace(artist)) + { + var key = Fingerprint(title, artist, album, duration); + var cover = _settings().SendCoverArt ? await ReadCoverAsync(media.Thumbnail, cancellationToken).ConfigureAwait(false) : null; + var fallbackLink = _settings().UseSearchLinkFallback + ? "https://open.spotify.com/search/" + Uri.EscapeDataString(string.Join(" ", new[] { title, artist }.Where(value => value.Length > 0))) + : ""; + var track = new MediaTrack(key, title, artist, album, "", fallbackLink, duration, cover); + _trackCache = track; + if (enrichTrack) _ = EnrichAndPublishAsync(track); + } + else + { + _trackCache = null; + } + } + else if (_trackCache is not null && _trackCache.DurationMilliseconds != duration) + { + _trackCache = _trackCache with { DurationMilliseconds = duration }; + } + + var snapshot = new MediaSnapshot(Id, status, position, duration, rate, _trackCache, capturedAt); + _last = snapshot; + return snapshot; + } + finally { _refreshLock.Release(); } + } + + + private async Task EnrichAndPublishAsync(MediaTrack track) + { + try + { + var enriched = await _enrich(track, CancellationToken.None).ConfigureAwait(false); + if (enriched == track) return; + await _refreshLock.WaitAsync().ConfigureAwait(false); + try + { + if (_trackCache?.Key != track.Key) return; + _trackCache = enriched; + } + finally { _refreshLock.Release(); } + await RefreshAndPublishAsync("metadata", false, CancellationToken.None).ConfigureAwait(false); + } + catch (Exception error) { _log("Spotify metadata enrichment update failed", error); } + } + + private static bool IsSpotifySession(GlobalSystemMediaTransportControlsSession session) + { + var source = session.SourceAppUserModelId ?? ""; + return source.Contains("spotify", StringComparison.OrdinalIgnoreCase); + } + + private static async Task ReadCoverAsync(IRandomAccessStreamReference? reference, CancellationToken cancellationToken) + { + if (reference is null) return null; + try + { + using var stream = await reference.OpenReadAsync(); + var length = checked((uint)Math.Min(stream.Size, MaxCoverInputBytes)); + if (length == 0) return null; + using var reader = new DataReader(stream.GetInputStreamAt(0)); + var loaded = await reader.LoadAsync(length); + cancellationToken.ThrowIfCancellationRequested(); + if (loaded == 0) return null; + var bytes = new byte[checked((int)loaded)]; + reader.ReadBytes(bytes); + var mime = string.IsNullOrWhiteSpace(stream.ContentType) ? "image/jpeg" : stream.ContentType; + return CreateCoverPayload(bytes, mime); + } + catch { return null; } + } + + + internal static CoverPayload? CreateCoverPayload(byte[] bytes, string sourceMime) + { + if (bytes.Length == 0 || bytes.Length > MaxCoverInputBytes) return null; + try + { + var encoded = ResizeJpeg(bytes, 512, 82L); + if (encoded.Length > MaxCoverOutputBytes) encoded = ResizeJpeg(bytes, 320, 68L); + if (encoded.Length == 0 || encoded.Length > MaxCoverOutputBytes) return null; + var colors = ExtractColors(encoded); + return new CoverPayload("image/jpeg", Convert.ToBase64String(encoded), colors.Primary, colors.Secondary); + } + catch { return null; } + } + + private static byte[] ResizeJpeg(byte[] bytes, int maximumDimension, long quality) + { + using var input = new MemoryStream(bytes); + using var original = new Bitmap(input); + var ratio = Math.Min(1d, Math.Min((double)maximumDimension / original.Width, (double)maximumDimension / original.Height)); + var width = Math.Max(1, (int)Math.Round(original.Width * ratio)); + var height = Math.Max(1, (int)Math.Round(original.Height * ratio)); + using var resized = new Bitmap(width, height, PixelFormat.Format24bppRgb); + using (var graphics = Graphics.FromImage(resized)) + { + graphics.Clear(Color.Black); + graphics.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality; + graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; + graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality; + graphics.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality; + graphics.DrawImage(original, 0, 0, width, height); + } + using var output = new MemoryStream(); + var codec = ImageCodecInfo.GetImageEncoders().First(item => item.FormatID == ImageFormat.Jpeg.Guid); + using var parameters = new EncoderParameters(1); + parameters.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, quality); + resized.Save(output, codec, parameters); + return output.ToArray(); + } + + internal static (string Primary, string Secondary) ExtractColors(byte[] bytes) + { + try + { + using var memory = new MemoryStream(bytes); + using var bitmap = new Bitmap(memory); + var samples = new List(); + var stepX = Math.Max(1, bitmap.Width / 12); + var stepY = Math.Max(1, bitmap.Height / 12); + for (var y = stepY / 2; y < bitmap.Height; y += stepY) + for (var x = stepX / 2; x < bitmap.Width; x += stepX) + { + var color = bitmap.GetPixel(x, y); + if (color.A > 100) samples.Add(color); + } + if (samples.Count == 0) return ("#1f2937", "#111827"); + var primary = Average(samples.OrderByDescending(c => Saturation(c)).Take(Math.Max(1, samples.Count / 3))); + var secondary = Average(samples.OrderBy(c => Brightness(c)).Take(Math.Max(1, samples.Count / 3))); + return (Hex(primary), Hex(secondary)); + } + catch { return ("#1f2937", "#111827"); } + } + + private static Color Average(IEnumerable colors) + { + var list = colors.ToList(); + return list.Count == 0 ? Color.FromArgb(31, 41, 55) : Color.FromArgb( + (int)list.Average(c => c.R), (int)list.Average(c => c.G), (int)list.Average(c => c.B)); + } + private static double Saturation(Color c) => c.GetSaturation(); + private static double Brightness(Color c) => c.GetBrightness(); + private static string Hex(Color c) => $"#{c.R:X2}{c.G:X2}{c.B:X2}"; + private static string Clean(string? value) => string.Join(" ", (value ?? "").Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries)).Trim(); + private static string Fingerprint(string title, string artist, string album, long duration) + { + var input = Encoding.UTF8.GetBytes($"{title}\u001f{artist}\u001f{album}\u001f{duration}"); + return Convert.ToHexString(SHA256.HashData(input)).ToLowerInvariant(); + } + + public async ValueTask DisposeAsync() + { + await StopAsync(CancellationToken.None).ConfigureAwait(false); + _refreshLock.Dispose(); + } +} diff --git a/companion/plugins/Lumi.Companion.SongOverlay/SongOverlayRuntime.cs b/companion/plugins/Lumi.Companion.SongOverlay/SongOverlayRuntime.cs new file mode 100644 index 0000000..28dfd50 --- /dev/null +++ b/companion/plugins/Lumi.Companion.SongOverlay/SongOverlayRuntime.cs @@ -0,0 +1,383 @@ +using Lumi.Companion.Abstractions; +using Lumi.Companion.SongOverlay.Providers; +using Lumi.Companion.SongOverlay.Spotify; + +namespace Lumi.Companion.SongOverlay; + +public sealed class SongOverlayRuntime : ICompanionPluginContribution, IAsyncDisposable +{ + private const string PluginId = "now_playing"; + private readonly SongOverlaySettingsStore _settingsStore; + private readonly SongOverlaySecretProtector _secrets = new(); + private readonly SongOverlayTransport _transport; + private readonly string _logDirectory; + private IMediaProvider? _provider; + private SpotifyWebApiEnricher? _spotify; + private CancellationTokenSource? _lifetime; + private PeriodicTimer? _heartbeat; + private Task? _heartbeatTask; + private readonly SemaphoreSlim _eventLock = new(1, 1); + private MediaSnapshot? _lastObserved; + private MediaSnapshot? _lastDelivered; + private readonly List _trackHistory = []; + private int _historyIndex = -1; + private long _sequence; + private readonly string _sessionId = Guid.NewGuid().ToString("N"); + private bool _initialized; + + public SongOverlayRuntime( + string pluginDataDirectory, + string logDirectory, + ICompanionPluginTransport hostTransport) + { + Directory.CreateDirectory(pluginDataDirectory); + _logDirectory = logDirectory; + _settingsStore = new SongOverlaySettingsStore(Path.Combine(pluginDataDirectory, "settings.json")); + _transport = new SongOverlayTransport(hostTransport); + Actions = + [ + new CompanionPluginAction("send", () => "Send current song now", SendSnapshotAsync, () => _initialized && Settings.Enabled, 10), + new CompanionPluginAction("toggle", () => Settings.Enabled ? "Disable monitoring" : "Enable monitoring", ToggleEnabledAsync, () => _initialized, 20) + ]; + } + + public CompanionPluginDescriptor Descriptor { get; } = new( + PluginId, + "Song Overlay", + new Version(0, 1, 1), + "Reads provider-neutral Windows media-session events and sends minimal playback changes to Lumi.", + 200); + + public IReadOnlyList Pages { get; } = + [ + new CompanionPluginPage("SongOverlay", "Overview & settings", 10) + ]; + + public IReadOnlyList Actions { get; } + public CompanionPluginStatus Status { get; private set; } = new(CompanionPluginHealth.Ready, "Starting", "Waiting for initialization."); + public SongOverlaySettings Settings => _settingsStore.Current; + public bool IsInitialized => _initialized; + public bool IsSpotifyEnrichmentConnected => _spotify?.IsConfigured == true; + public bool UsesCompanionAuthentication => _transport.IsConfigured; + public string ProviderStatus => Status.Detail ?? Status.Summary; + public string? CurrentTrack => _lastObserved?.Track is null ? null : $"{_lastObserved.Track.Title} — {_lastObserved.Track.Artist}"; + public Uri? EffectiveLumiBaseUri => _transport.BaseUri; + public event Action? Changed; + + public async Task InitializeAsync(CancellationToken cancellationToken = default) + { + await _settingsStore.LoadAsync(cancellationToken).ConfigureAwait(false); + if (string.IsNullOrWhiteSpace(Settings.ProviderId)) Settings.ProviderId = "spotify"; + // Re-save through the current schema so legacy plugin-specific Lumi host + // and connection-key fields are removed after the shared transport upgrade. + await _settingsStore.SaveAsync(Settings, cancellationToken).ConfigureAwait(false); + _spotify = new SpotifyWebApiEnricher(Settings, _secrets, SaveSettingsSync, Log); + _initialized = true; + await RestartProviderAsync(cancellationToken).ConfigureAwait(false); + } + + public async Task SaveSettingsAsync(bool restartProvider, CancellationToken cancellationToken = default) + { + Settings.HeartbeatSeconds = Math.Clamp(Settings.HeartbeatSeconds, 15, 300); + Settings.SeekThresholdMilliseconds = Math.Clamp(Settings.SeekThresholdMilliseconds, 500, 10000); + Settings.ProviderId = string.IsNullOrWhiteSpace(Settings.ProviderId) ? "spotify" : Settings.ProviderId.Trim().ToLowerInvariant(); + await _settingsStore.SaveAsync(Settings, cancellationToken).ConfigureAwait(false); + if (restartProvider) await RestartProviderAsync(cancellationToken).ConfigureAwait(false); + RaiseChanged(); + } + + public async Task ToggleEnabledAsync(CancellationToken cancellationToken = default) + { + Settings.Enabled = !Settings.Enabled; + await SaveSettingsAsync(restartProvider: true, cancellationToken).ConfigureAwait(false); + } + + public async Task ConnectSpotifyAsync(CancellationToken cancellationToken = default) + { + EnsureInitialized(); + await _spotify!.AuthorizeAsync(cancellationToken).ConfigureAwait(false); + SaveSettingsSync(); + RaiseChanged(); + if (_transport.IsConfigured) + await SendSnapshotAsync(cancellationToken).ConfigureAwait(false); + } + + public void DisconnectSpotify() + { + _spotify?.Disconnect(); + RaiseChanged(); + } + + public async Task SendSnapshotAsync(CancellationToken cancellationToken = default) + { + EnsureInitialized(); + if (!_transport.IsConfigured) throw new InvalidOperationException("Pair Companion with Lumi before sending song updates."); + if (_provider is null) throw new InvalidOperationException("No media provider is running."); + var snapshot = await _provider.GetSnapshotAsync(cancellationToken).ConfigureAwait(false); + if (snapshot is null) throw new InvalidOperationException("Spotify is not currently exposing playback information."); + await SendAsync("snapshot", snapshot, includeTrack: true, cancellationToken).ConfigureAwait(false); + } + + private async Task RestartProviderAsync(CancellationToken cancellationToken) + { + await StopProviderAsync().ConfigureAwait(false); + if (!Settings.Enabled) + { + SetStatus(CompanionPluginHealth.Ready, "Disabled", "Song Overlay monitoring is disabled."); + return; + } + _lastObserved = null; + _lastDelivered = null; + _lifetime = new CancellationTokenSource(); + _provider = Settings.ProviderId switch + { + "spotify" => new SpotifyWindowsMediaProvider(() => Settings, EnrichTrackAsync, Log), + _ => throw new InvalidOperationException($"Provider '{Settings.ProviderId}' is not installed.") + }; + _provider.StateChanged += OnProviderStateChanged; + _provider.AvailabilityChanged += OnAvailabilityChanged; + try + { + await _provider.StartAsync(cancellationToken).ConfigureAwait(false); + StartHeartbeat(); + var initial = await _provider.GetSnapshotAsync(cancellationToken).ConfigureAwait(false); + if (!_transport.IsConfigured) + SetStatus(CompanionPluginHealth.Warning, "Pairing required", "Spotify monitoring is ready, but Companion must be paired before Lumi can receive updates."); + else if (initial is null) + SetStatus(CompanionPluginHealth.Warning, "Waiting for Spotify", "Spotify is not currently exposing a Windows media session."); + else + SetStatus(CompanionPluginHealth.Healthy, "Monitoring", Describe(initial)); + } + catch (Exception error) + { + SetStatus(CompanionPluginHealth.Error, "Provider failed", "Could not start the Spotify media provider: " + error.Message); + throw; + } + } + + private async Task StopProviderAsync() + { + _lifetime?.Cancel(); + _heartbeat?.Dispose(); + if (_heartbeatTask is not null) + { + try { await _heartbeatTask.ConfigureAwait(false); } catch { } + } + _heartbeat = null; + _heartbeatTask = null; + if (_provider is not null) + { + _provider.StateChanged -= OnProviderStateChanged; + _provider.AvailabilityChanged -= OnAvailabilityChanged; + await _provider.DisposeAsync().ConfigureAwait(false); + } + _provider = null; + _lifetime?.Dispose(); + _lifetime = null; + } + + private void StartHeartbeat() + { + _heartbeat = new PeriodicTimer(TimeSpan.FromSeconds(Math.Clamp(Settings.HeartbeatSeconds, 15, 300))); + var token = _lifetime?.Token ?? CancellationToken.None; + _heartbeatTask = Task.Run(async () => + { + try + { + while (_heartbeat is not null && await _heartbeat.WaitForNextTickAsync(token).ConfigureAwait(false)) + { + var current = _lastObserved; + if (current is null || !_transport.IsConfigured) continue; + await SendAsync("heartbeat", current, includeTrack: _lastDelivered?.Track?.Key != current.Track?.Key, token).ConfigureAwait(false); + } + } + catch (OperationCanceledException) { } + catch (Exception error) { Log("Song Overlay heartbeat failed", error); } + }, token); + } + + private void OnAvailabilityChanged(object? sender, string message) + { + if (!_transport.IsConfigured) + { + SetStatus(CompanionPluginHealth.Warning, "Pairing required", message); + return; + } + var waiting = message.Contains("not exposing", StringComparison.OrdinalIgnoreCase); + SetStatus(waiting ? CompanionPluginHealth.Warning : CompanionPluginHealth.Healthy, + waiting ? "Waiting for Spotify" : "Monitoring", message); + } + + private void OnProviderStateChanged(object? sender, ProviderStateChangedEventArgs args) => + _ = HandleStateChangeAsync(args.Reason, args.Snapshot, _lifetime?.Token ?? CancellationToken.None); + + private async Task HandleStateChangeAsync(string reason, MediaSnapshot? snapshot, CancellationToken cancellationToken) + { + await _eventLock.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + if (snapshot is null) + { + if (_lastObserved is null) return; + var stopped = _lastObserved with { Status = "stopped", PositionMilliseconds = 0, Track = null, CapturedAt = DateTimeOffset.UtcNow }; + _lastObserved = null; + if (_transport.IsConfigured) await SendAsync("stop", stopped, false, cancellationToken).ConfigureAwait(false); + else RaiseChanged(); + return; + } + var previous = _lastObserved; + _lastObserved = snapshot; + RaiseChanged(); + if (!_transport.IsConfigured) + { + SetStatus(CompanionPluginHealth.Warning, "Pairing required", Describe(snapshot)); + return; + } + var currentTrackKey = snapshot.Track?.Key; + var trackChanged = !string.IsNullOrWhiteSpace(currentTrackKey) + && !string.Equals(currentTrackKey, previous?.Track?.Key, StringComparison.Ordinal); + if (trackChanged) + { + await SendAsync(ClassifyTrackDirection(currentTrackKey!), snapshot, true, cancellationToken).ConfigureAwait(false); + return; + } + if (previous is null) + { + await SendAsync("snapshot", snapshot, true, cancellationToken).ConfigureAwait(false); + return; + } + if (reason == "metadata") + { + await SendAsync("snapshot", snapshot, true, cancellationToken).ConfigureAwait(false); + return; + } + if (!string.Equals(previous.Status, snapshot.Status, StringComparison.Ordinal)) + { + var playbackEvent = snapshot.Status switch + { + "paused" => "pause", + "stopped" or "closed" => "stop", + "playing" when previous.Status == "paused" => "resume", + "playing" => "play", + _ => "snapshot" + }; + await SendAsync(playbackEvent, snapshot, false, cancellationToken).ConfigureAwait(false); + return; + } + if (reason == "timeline") + { + var expected = ProjectPosition(previous, snapshot.CapturedAt); + if (Math.Abs(snapshot.PositionMilliseconds - expected) >= Math.Clamp(Settings.SeekThresholdMilliseconds, 500, 10000)) + await SendAsync("seek", snapshot, false, cancellationToken).ConfigureAwait(false); + } + } + catch (OperationCanceledException) { } + catch (Exception error) + { + Log("Could not process a Song Overlay event", error); + SetStatus(CompanionPluginHealth.Warning, "Delivery delayed", "Playback was detected, but Lumi could not be updated: " + error.Message); + } + finally { _eventLock.Release(); } + } + + private async Task SendAsync(string eventName, MediaSnapshot snapshot, bool includeTrack, CancellationToken cancellationToken) + { + var track = includeTrack && snapshot.Track is not null + ? new TrackPayload(snapshot.Track.Key, snapshot.Track.Title, snapshot.Track.Artist, snapshot.Track.Album, + snapshot.Track.ReleaseYear, snapshot.Track.Link, snapshot.Track.Cover) + : null; + var payload = new NowPlayingEventPayload( + 1, + snapshot.Provider, + _sessionId, + Interlocked.Increment(ref _sequence), + eventName, + snapshot.CapturedAt.ToUnixTimeMilliseconds(), + new PlaybackPayload(snapshot.Status, snapshot.PositionMilliseconds, snapshot.DurationMilliseconds, snapshot.PlaybackRate), + track); + var response = await _transport.SendJsonAsync("/plugins/now_playing/api/companion/state", payload, cancellationToken).ConfigureAwait(false); + if (!response.IsSuccessStatusCode) + { + throw new InvalidOperationException($"Lumi rejected the update with HTTP {response.StatusCode}: {Trim(response.Body, 240)}"); + } + _lastDelivered = snapshot; + SetStatus(CompanionPluginHealth.Healthy, snapshot.Status == "playing" ? "Playing" : "Connected", Describe(snapshot)); + } + + private async Task EnrichTrackAsync(MediaTrack track, CancellationToken cancellationToken) + { + if (_spotify?.IsConfigured != true) return track; + SpotifyEnrichment? enriched = null; + foreach (var delay in new[] { 0, 500, 1000 }) + { + if (delay > 0) await Task.Delay(delay, cancellationToken).ConfigureAwait(false); + enriched = await _spotify.EnrichAsync(track, cancellationToken).ConfigureAwait(false); + if (enriched is not null) break; + } + if (enriched is null) return track; + CoverPayload? cover = track.Cover; + if (enriched.CoverBytes is { Length: > 0 } bytes && Settings.SendCoverArt) + cover = SpotifyWindowsMediaProvider.CreateCoverPayload(bytes, enriched.CoverMime) ?? cover; + return track with + { + Link = string.IsNullOrWhiteSpace(enriched.Link) ? track.Link : enriched.Link, + ReleaseYear = string.IsNullOrWhiteSpace(enriched.ReleaseYear) ? track.ReleaseYear : enriched.ReleaseYear, + Cover = cover + }; + } + + private string ClassifyTrackDirection(string key) + { + if (_historyIndex > 0 && _trackHistory[_historyIndex - 1] == key) { _historyIndex--; return "previous"; } + if (_historyIndex >= 0 && _historyIndex + 1 < _trackHistory.Count && _trackHistory[_historyIndex + 1] == key) { _historyIndex++; return "next"; } + if (_historyIndex + 1 < _trackHistory.Count) _trackHistory.RemoveRange(_historyIndex + 1, _trackHistory.Count - _historyIndex - 1); + _trackHistory.Add(key); + if (_trackHistory.Count > 50) _trackHistory.RemoveAt(0); + _historyIndex = _trackHistory.Count - 1; + return "track_changed"; + } + + private static long ProjectPosition(MediaSnapshot snapshot, DateTimeOffset at) + { + if (snapshot.Status != "playing") return snapshot.PositionMilliseconds; + var elapsed = Math.Max(0, (at - snapshot.CapturedAt).TotalMilliseconds); + return Math.Min(snapshot.DurationMilliseconds > 0 ? snapshot.DurationMilliseconds : long.MaxValue, + snapshot.PositionMilliseconds + (long)(elapsed * snapshot.PlaybackRate)); + } + + private void SaveSettingsSync() => _settingsStore.Save(Settings); + private void EnsureInitialized() + { + if (!_initialized) throw new InvalidOperationException("The Song Overlay companion plugin has not initialized yet."); + } + + private void SetStatus(CompanionPluginHealth health, string summary, string detail) + { + Status = new CompanionPluginStatus(health, summary, detail); + RaiseChanged(); + } + + private void RaiseChanged() => Changed?.Invoke(); + private void Log(string message, Exception? error = null) + { + try + { + Directory.CreateDirectory(_logDirectory); + File.AppendAllText(Path.Combine(_logDirectory, $"song-overlay-{DateTime.UtcNow:yyyyMMdd}.log"), + $"{DateTimeOffset.Now:O}\t{message}{(error is null ? "" : "\t" + error)}{Environment.NewLine}"); + } + catch { } + } + + private static string Describe(MediaSnapshot snapshot) => snapshot.Track is null + ? "Connected; no active song." + : $"{snapshot.Status}: {snapshot.Track.Title} — {snapshot.Track.Artist}"; + private static string Trim(string value, int max) => value.Length <= max ? value : value[..max]; + + public async ValueTask DisposeAsync() + { + await StopProviderAsync().ConfigureAwait(false); + _spotify?.Dispose(); + _eventLock.Dispose(); + } +} diff --git a/companion/plugins/Lumi.Companion.SongOverlay/SongOverlaySecretProtector.cs b/companion/plugins/Lumi.Companion.SongOverlay/SongOverlaySecretProtector.cs new file mode 100644 index 0000000..a5b6d64 --- /dev/null +++ b/companion/plugins/Lumi.Companion.SongOverlay/SongOverlaySecretProtector.cs @@ -0,0 +1,28 @@ +using System.Security.Cryptography; +using System.Text; + +namespace Lumi.Companion.SongOverlay; + +internal sealed class SongOverlaySecretProtector +{ + private static readonly byte[] Entropy = "Lumi.Companion.SongOverlay.v1"u8.ToArray(); + + public string Protect(string value) + { + if (string.IsNullOrWhiteSpace(value)) return ""; + return Convert.ToBase64String(ProtectedData.Protect(Encoding.UTF8.GetBytes(value), Entropy, DataProtectionScope.CurrentUser)); + } + + public string Unprotect(string value) + { + if (string.IsNullOrWhiteSpace(value)) return ""; + try + { + return Encoding.UTF8.GetString(ProtectedData.Unprotect(Convert.FromBase64String(value), Entropy, DataProtectionScope.CurrentUser)); + } + catch (CryptographicException) + { + return ""; + } + } +} diff --git a/companion/plugins/Lumi.Companion.SongOverlay/SongOverlaySettings.cs b/companion/plugins/Lumi.Companion.SongOverlay/SongOverlaySettings.cs new file mode 100644 index 0000000..121dc9b --- /dev/null +++ b/companion/plugins/Lumi.Companion.SongOverlay/SongOverlaySettings.cs @@ -0,0 +1,13 @@ +namespace Lumi.Companion.SongOverlay; + +public sealed class SongOverlaySettings +{ + public bool Enabled { get; set; } = true; + public string ProviderId { get; set; } = "spotify"; + public int HeartbeatSeconds { get; set; } = 30; + public int SeekThresholdMilliseconds { get; set; } = 1500; + public bool SendCoverArt { get; set; } = true; + public bool UseSearchLinkFallback { get; set; } = true; + public string SpotifyClientId { get; set; } = ""; + public string ProtectedSpotifyRefreshToken { get; set; } = ""; +} diff --git a/companion/plugins/Lumi.Companion.SongOverlay/SongOverlaySettingsStore.cs b/companion/plugins/Lumi.Companion.SongOverlay/SongOverlaySettingsStore.cs new file mode 100644 index 0000000..7b14210 --- /dev/null +++ b/companion/plugins/Lumi.Companion.SongOverlay/SongOverlaySettingsStore.cs @@ -0,0 +1,68 @@ +using System.Text.Json; + +namespace Lumi.Companion.SongOverlay; + +internal sealed class SongOverlaySettingsStore(string path) +{ + private readonly SemaphoreSlim _gate = new(1, 1); + public SongOverlaySettings Current { get; private set; } = new(); + + public async Task LoadAsync(CancellationToken cancellationToken = default) + { + if (!File.Exists(path)) return; + try + { + Current = JsonSerializer.Deserialize( + await File.ReadAllBytesAsync(path, cancellationToken), + SongOverlayJson.Options) ?? new(); + } + catch (JsonException) + { + Current = new(); + } + } + + public void Save(SongOverlaySettings settings) + { + _gate.Wait(); + try + { + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + var temporary = $"{path}.{Environment.ProcessId}.tmp"; + try + { + File.WriteAllBytes(temporary, JsonSerializer.SerializeToUtf8Bytes(settings, SongOverlayJson.Options)); + File.Move(temporary, path, true); + } + finally { File.Delete(temporary); } + Current = settings; + } + finally { _gate.Release(); } + } + + public async Task SaveAsync(SongOverlaySettings settings, CancellationToken cancellationToken = default) + { + await _gate.WaitAsync(cancellationToken); + try + { + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + var temporary = $"{path}.{Environment.ProcessId}.tmp"; + try + { + await File.WriteAllBytesAsync(temporary, JsonSerializer.SerializeToUtf8Bytes(settings, SongOverlayJson.Options), cancellationToken); + File.Move(temporary, path, true); + } + finally { File.Delete(temporary); } + Current = settings; + } + finally { _gate.Release(); } + } +} + +internal static class SongOverlayJson +{ + public static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web) + { + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull + }; +} diff --git a/companion/plugins/Lumi.Companion.SongOverlay/SongOverlayTransport.cs b/companion/plugins/Lumi.Companion.SongOverlay/SongOverlayTransport.cs new file mode 100644 index 0000000..4ef7468 --- /dev/null +++ b/companion/plugins/Lumi.Companion.SongOverlay/SongOverlayTransport.cs @@ -0,0 +1,16 @@ +using Lumi.Companion.Abstractions; + +namespace Lumi.Companion.SongOverlay; + +internal sealed class SongOverlayTransport(ICompanionPluginTransport host) +{ + public Uri? BaseUri => host.LumiBaseUri; + + public bool IsConfigured => host.IsAuthenticated; + + public Task SendJsonAsync( + string relativePath, + object payload, + CancellationToken cancellationToken = default) => + host.PostJsonAsync(relativePath, payload, cancellationToken); +} diff --git a/companion/plugins/Lumi.Companion.SongOverlay/Spotify/SpotifyWebApiEnricher.cs b/companion/plugins/Lumi.Companion.SongOverlay/Spotify/SpotifyWebApiEnricher.cs new file mode 100644 index 0000000..a369496 --- /dev/null +++ b/companion/plugins/Lumi.Companion.SongOverlay/Spotify/SpotifyWebApiEnricher.cs @@ -0,0 +1,256 @@ +using System.Diagnostics; +using System.Net; +using System.Net.Http.Headers; +using System.Net.Sockets; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; + +namespace Lumi.Companion.SongOverlay.Spotify; + +internal sealed record SpotifyEnrichment(string Link, string ReleaseYear, byte[]? CoverBytes, string CoverMime); + +internal sealed class SpotifyWebApiEnricher : IDisposable +{ + private const string Scope = "user-read-currently-playing"; + private readonly HttpClient _http = new() { Timeout = TimeSpan.FromSeconds(15) }; + private readonly SongOverlaySecretProtector _secrets; + private readonly Action _save; + private readonly Action _log; + private readonly SongOverlaySettings _settings; + private string _accessToken = ""; + private DateTimeOffset _accessTokenExpiresAt = DateTimeOffset.MinValue; + + public SpotifyWebApiEnricher(SongOverlaySettings settings, SongOverlaySecretProtector secrets, Action save, Action log) + { + _settings = settings; + _secrets = secrets; + _save = save; + _log = log; + _http.DefaultRequestHeaders.UserAgent.ParseAdd("Lumi-Companion-NowPlaying/0.1.0"); + } + + public bool IsConfigured => !string.IsNullOrWhiteSpace(_settings.SpotifyClientId) && !string.IsNullOrWhiteSpace(_settings.ProtectedSpotifyRefreshToken); + + public async Task AuthorizeAsync(CancellationToken cancellationToken) + { + var clientId = _settings.SpotifyClientId.Trim(); + if (string.IsNullOrWhiteSpace(clientId)) throw new InvalidOperationException("Enter your Spotify application Client ID first."); + + var verifier = Base64Url(RandomNumberGenerator.GetBytes(64)); + var challenge = Base64Url(SHA256.HashData(Encoding.ASCII.GetBytes(verifier))); + var state = Base64Url(RandomNumberGenerator.GetBytes(24)); + var port = FindFreePort(); + var redirectUri = $"http://127.0.0.1:{port}/callback/"; + using var listener = new HttpListener(); + listener.Prefixes.Add(redirectUri); + listener.Start(); + + var authorizationUrl = "https://accounts.spotify.com/authorize?" + BuildQuery(new Dictionary + { + ["client_id"] = clientId, + ["response_type"] = "code", + ["redirect_uri"] = redirectUri, + ["scope"] = Scope, + ["code_challenge_method"] = "S256", + ["code_challenge"] = challenge, + ["state"] = state + }); + Process.Start(new ProcessStartInfo(authorizationUrl) { UseShellExecute = true }); + + using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeout.CancelAfter(TimeSpan.FromMinutes(3)); + var contextTask = listener.GetContextAsync(); + var completed = await Task.WhenAny(contextTask, Task.Delay(Timeout.InfiniteTimeSpan, timeout.Token)).ConfigureAwait(false); + if (completed != contextTask) throw new TimeoutException("Spotify authorization timed out."); + var context = await contextTask.ConfigureAwait(false); + var query = context.Request.QueryString; + var responseText = "Spotify is connected to Lumi Companion. You can close this page."; + try + { + if (!string.Equals(query["state"], state, StringComparison.Ordinal)) throw new InvalidOperationException("Spotify returned an invalid authorization state."); + if (!string.IsNullOrWhiteSpace(query["error"])) throw new InvalidOperationException("Spotify authorization was declined: " + query["error"]); + var code = query["code"] ?? throw new InvalidOperationException("Spotify did not return an authorization code."); + var token = await ExchangeAsync(new Dictionary + { + ["client_id"] = clientId, + ["grant_type"] = "authorization_code", + ["code"] = code, + ["redirect_uri"] = redirectUri, + ["code_verifier"] = verifier + }, cancellationToken).ConfigureAwait(false); + ApplyToken(token); + if (string.IsNullOrWhiteSpace(token.RefreshToken)) throw new InvalidOperationException("Spotify did not provide a refresh token."); + _settings.ProtectedSpotifyRefreshToken = _secrets.Protect(token.RefreshToken); + _save(); + } + catch + { + responseText = "Spotify could not be connected to Lumi Companion. Return to the app for details."; + throw; + } + finally + { + var bytes = Encoding.UTF8.GetBytes($"Lumi Companion

Lumi Companion

{WebUtility.HtmlEncode(responseText)}

"); + context.Response.ContentType = "text/html; charset=utf-8"; + context.Response.ContentLength64 = bytes.Length; + await context.Response.OutputStream.WriteAsync(bytes, cancellationToken).ConfigureAwait(false); + context.Response.Close(); + } + } + + public void Disconnect() + { + _accessToken = ""; + _accessTokenExpiresAt = DateTimeOffset.MinValue; + _settings.ProtectedSpotifyRefreshToken = ""; + _save(); + } + + public async Task EnrichAsync(MediaTrack track, CancellationToken cancellationToken) + { + if (!IsConfigured) return null; + try + { + var token = await GetAccessTokenAsync(cancellationToken).ConfigureAwait(false); + if (string.IsNullOrWhiteSpace(token)) return null; + using var response = await SendCurrentlyPlayingAsync(token, cancellationToken).ConfigureAwait(false); + if (response.StatusCode == HttpStatusCode.NoContent) return null; + if (response.StatusCode == HttpStatusCode.Unauthorized) + { + _accessToken = ""; + token = await GetAccessTokenAsync(cancellationToken).ConfigureAwait(false); + if (string.IsNullOrWhiteSpace(token)) return null; + using var retry = await SendCurrentlyPlayingAsync(token, cancellationToken).ConfigureAwait(false); + if (retry.StatusCode == HttpStatusCode.NoContent || !retry.IsSuccessStatusCode) return null; + return await ParseEnrichmentAsync(retry, track, cancellationToken).ConfigureAwait(false); + } + if (!response.IsSuccessStatusCode) return null; + return await ParseEnrichmentAsync(response, track, cancellationToken).ConfigureAwait(false); + } + catch (Exception error) + { + _log("Spotify metadata enrichment failed", error); + return null; + } + } + + + private async Task SendCurrentlyPlayingAsync(string token, CancellationToken cancellationToken) + { + using var request = new HttpRequestMessage(HttpMethod.Get, "https://api.spotify.com/v1/me/player/currently-playing?additional_types=track"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + return await _http.SendAsync(request, cancellationToken).ConfigureAwait(false); + } + + private async Task ParseEnrichmentAsync(HttpResponseMessage response, MediaTrack track, CancellationToken cancellationToken) + { + using var document = JsonDocument.Parse(await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false)); + if (!document.RootElement.TryGetProperty("item", out var item) || item.ValueKind != JsonValueKind.Object) return null; + var title = String(item, "name"); + var artists = item.TryGetProperty("artists", out var artistArray) && artistArray.ValueKind == JsonValueKind.Array + ? string.Join(", ", artistArray.EnumerateArray().Select(value => String(value, "name")).Where(value => value.Length > 0)) + : ""; + if (!LooseMatch(track.Title, title) || (!string.IsNullOrWhiteSpace(track.Artist) && !LooseMatch(track.Artist, artists))) return null; + var link = item.TryGetProperty("external_urls", out var urls) ? String(urls, "spotify") : ""; + var releaseYear = ""; + string imageUrl = ""; + if (item.TryGetProperty("album", out var album)) + { + var releaseDate = String(album, "release_date"); + if (releaseDate.Length >= 4) releaseYear = releaseDate[..4]; + if (album.TryGetProperty("images", out var images) && images.ValueKind == JsonValueKind.Array) + imageUrl = images.EnumerateArray().Select(value => String(value, "url")).FirstOrDefault(value => value.Length > 0) ?? ""; + } + byte[]? cover = null; + var mime = "image/jpeg"; + if (!string.IsNullOrWhiteSpace(imageUrl)) + { + using var imageResponse = await _http.GetAsync(imageUrl, cancellationToken).ConfigureAwait(false); + if (imageResponse.IsSuccessStatusCode) + { + cover = await imageResponse.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); + if (cover.Length > 3 * 1024 * 1024) cover = null; + mime = imageResponse.Content.Headers.ContentType?.MediaType ?? mime; + } + } + return new SpotifyEnrichment(link, releaseYear, cover, mime); + } + + private async Task GetAccessTokenAsync(CancellationToken cancellationToken) + { + if (!string.IsNullOrWhiteSpace(_accessToken) && _accessTokenExpiresAt > DateTimeOffset.UtcNow.AddMinutes(1)) return _accessToken; + var refreshToken = _secrets.Unprotect(_settings.ProtectedSpotifyRefreshToken); + if (string.IsNullOrWhiteSpace(refreshToken)) return ""; + try + { + var token = await ExchangeAsync(new Dictionary + { + ["client_id"] = _settings.SpotifyClientId.Trim(), + ["grant_type"] = "refresh_token", + ["refresh_token"] = refreshToken + }, cancellationToken).ConfigureAwait(false); + ApplyToken(token); + if (!string.IsNullOrWhiteSpace(token.RefreshToken)) + { + _settings.ProtectedSpotifyRefreshToken = _secrets.Protect(token.RefreshToken); + _save(); + } + return _accessToken; + } + catch (SpotifyInvalidGrantException) + { + Disconnect(); + return ""; + } + } + + private async Task ExchangeAsync(Dictionary values, CancellationToken cancellationToken) + { + using var response = await _http.PostAsync("https://accounts.spotify.com/api/token", new FormUrlEncodedContent(values), cancellationToken).ConfigureAwait(false); + var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + if (!response.IsSuccessStatusCode) + { + if (body.Contains("invalid_grant", StringComparison.OrdinalIgnoreCase)) throw new SpotifyInvalidGrantException(); + throw new InvalidOperationException($"Spotify token exchange failed with HTTP {(int)response.StatusCode}."); + } + using var document = JsonDocument.Parse(body); + return new TokenResponse( + String(document.RootElement, "access_token"), + String(document.RootElement, "refresh_token"), + document.RootElement.TryGetProperty("expires_in", out var expires) ? expires.GetInt32() : 3600); + } + + private void ApplyToken(TokenResponse token) + { + _accessToken = token.AccessToken; + _accessTokenExpiresAt = DateTimeOffset.UtcNow.AddSeconds(Math.Max(60, token.ExpiresIn)); + } + + private static string String(JsonElement element, string property) => + element.TryGetProperty(property, out var value) && value.ValueKind == JsonValueKind.String ? value.GetString() ?? "" : ""; + + private static bool LooseMatch(string left, string right) + { + static string Normalize(string value) => new(value.ToLowerInvariant().Where(char.IsLetterOrDigit).ToArray()); + var a = Normalize(left); var b = Normalize(right); + return a.Length > 0 && b.Length > 0 && (a == b || a.Contains(b, StringComparison.Ordinal) || b.Contains(a, StringComparison.Ordinal)); + } + + private static int FindFreePort() + { + var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + var port = ((IPEndPoint)listener.LocalEndpoint).Port; + listener.Stop(); + return port; + } + + private static string BuildQuery(IEnumerable> values) => + string.Join("&", values.Select(pair => $"{Uri.EscapeDataString(pair.Key)}={Uri.EscapeDataString(pair.Value)}")); + private static string Base64Url(byte[] bytes) => Convert.ToBase64String(bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_'); + + public void Dispose() => _http.Dispose(); + private sealed record TokenResponse(string AccessToken, string RefreshToken, int ExpiresIn); + private sealed class SpotifyInvalidGrantException : Exception { } +} diff --git a/companion/plugins/Lumi.Companion.SongOverlay/plugin.json b/companion/plugins/Lumi.Companion.SongOverlay/plugin.json new file mode 100644 index 0000000..5783816 --- /dev/null +++ b/companion/plugins/Lumi.Companion.SongOverlay/plugin.json @@ -0,0 +1,16 @@ +{ + "id": "now_playing", + "name": "Song Overlay", + "version": "0.1.2", + "provider_api": 1, + "providers": ["spotify"], + "capabilities": [ + "windows.media-session.read", + "network.lumi", + "network.spotify.optional" + ], + "navigation": { + "root": "Song Overlay", + "pages": ["SongOverlay"] + } +} diff --git a/companion/scripts/build-obs-bridge.ps1 b/companion/scripts/build-obs-bridge.ps1 index b085c9f..99afb69 100644 --- a/companion/scripts/build-obs-bridge.ps1 +++ b/companion/scripts/build-obs-bridge.ps1 @@ -1,6 +1,6 @@ param( [string]$ObsVersion = "31.1.1", - [string]$BridgeVersion = "0.1.0-experimental.5", + [string]$BridgeVersion = "0.1.0", [string]$CacheRoot = "$env:LOCALAPPDATA\LumiCompanionBuild" ) @@ -79,11 +79,19 @@ New-Item -ItemType Directory -Force -Path $importRoot | Out-Null New-ImportLibrary (Join-Path $runtimeRoot "bin\64bit\obs.dll") "obs" $dumpbin $lib New-ImportLibrary (Join-Path $runtimeRoot "bin\64bit\obs-frontend-api.dll") "obs-frontend-api" $dumpbin $lib -$configureArguments = @("-S", "`"$nativeRoot`"", "-B", "`"$buildRoot`"", "-G", "`"Visual Studio 17 2022`"", "-A", "x64", "`"-DOBS_SOURCE_DIR=$sourceRoot`"", "`"-DOBS_IMPORT_DIR=$importRoot`"", "`"-DLUMI_BRIDGE_VERSION=$BridgeVersion`"") -$configured = Start-Process -FilePath $cmake -ArgumentList $configureArguments -NoNewWindow -Wait -PassThru -if ($configured.ExitCode) { throw "OBS bridge configuration failed." } -$built = Start-Process -FilePath $cmake -ArgumentList @("--build", "`"$buildRoot`"", "--config", "Release") -NoNewWindow -Wait -PassThru -if ($built.ExitCode) { throw "OBS bridge build failed." } +$configureArguments = @( + "-S", $nativeRoot, + "-B", $buildRoot, + "-G", "Visual Studio 17 2022", + "-A", "x64", + "-DOBS_SOURCE_DIR=$sourceRoot", + "-DOBS_IMPORT_DIR=$importRoot", + "-DLUMI_BRIDGE_VERSION=$BridgeVersion" +) +& $cmake @configureArguments +if ($LASTEXITCODE) { throw "OBS bridge configuration failed." } +& $cmake --build $buildRoot --config Release +if ($LASTEXITCODE) { throw "OBS bridge build failed." } New-Item -ItemType Directory -Force -Path $componentRoot | Out-Null $bridgeDll = Join-Path $buildRoot "Release\lumi-obs-bridge.dll" diff --git a/companion/scripts/collect-nuget-notices.ps1 b/companion/scripts/collect-nuget-notices.ps1 new file mode 100644 index 0000000..657e0b8 --- /dev/null +++ b/companion/scripts/collect-nuget-notices.ps1 @@ -0,0 +1,98 @@ +param( + [Parameter(Mandatory = $true)][string]$RepoRoot, + [Parameter(Mandatory = $true)][string]$OutputRoot +) + +$ErrorActionPreference = "Stop" +$repo = (Resolve-Path $RepoRoot).Path +$assetFiles = @(Get-ChildItem (Join-Path $repo "companion") -Filter project.assets.json -File -Recurse -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -notmatch '[\\/]installer[\\/]output[\\/]' }) +if ($assetFiles.Count -eq 0) { throw "No restored NuGet project.assets.json files were found after publish." } + +Remove-Item $OutputRoot -Recurse -Force -ErrorAction SilentlyContinue +New-Item -ItemType Directory -Force -Path $OutputRoot | Out-Null + + +function Get-ChildText([System.Xml.XmlNode]$Node, [string]$Name) { + if (!$Node) { return $null } + $child = $Node.SelectSingleNode("*[local-name()='$Name']") + if (!$child) { return $null } + return $child.InnerText.Trim() +} + +$packages = @{} +foreach ($assetFile in $assetFiles) { + $assets = Get-Content $assetFile.FullName -Raw | ConvertFrom-Json + $packageFolders = @($assets.packageFolders.PSObject.Properties.Name) + foreach ($libraryProperty in $assets.libraries.PSObject.Properties) { + if ($libraryProperty.Value.type -ne "package") { continue } + $parts = $libraryProperty.Name -split '/', 2 + if ($parts.Count -ne 2) { continue } + $id = $parts[0] + $version = $parts[1] + $key = "$($id.ToLowerInvariant())/$($version.ToLowerInvariant())" + if ($packages.ContainsKey($key)) { continue } + + $packageRoot = $null + foreach ($folder in $packageFolders) { + $candidate = Join-Path $folder ($key -replace '/', [IO.Path]::DirectorySeparatorChar) + if (Test-Path $candidate) { $packageRoot = (Resolve-Path $candidate).Path; break } + } + if (!$packageRoot) { throw "Restored package files are missing for $id $version." } + $packages[$key] = [ordered]@{ Id = $id; Version = $version; Root = $packageRoot } + } +} + +$inventory = [Collections.Generic.List[string]]::new() +$inventory.Add("LUMI COMPANION - EXACT NUGET PACKAGE LICENCE INVENTORY") +$inventory.Add("Generated: $([DateTime]::UtcNow.ToString('yyyy-MM-dd HH:mm:ss')) UTC") +$inventory.Add("") +$inventory.Add("This file is generated from the restored project.assets.json files used by the Companion build. Package-specific licence, notice, copying, and copyright files found in the restored packages are copied into the adjacent package directories.") +$inventory.Add("") + +foreach ($package in @($packages.Values | Sort-Object Id, Version)) { + $safeName = ("$($package.Id)-$($package.Version)" -replace '[^A-Za-z0-9._-]', '_') + $packageOutput = Join-Path $OutputRoot $safeName + New-Item -ItemType Directory -Force -Path $packageOutput | Out-Null + + $nuspec = Get-ChildItem $package.Root -Filter *.nuspec -File | Select-Object -First 1 + $metadata = $null + if ($nuspec) { + [xml]$nuspecXml = Get-Content $nuspec.FullName -Raw + $metadata = $nuspecXml.SelectSingleNode("/*[local-name()='package']/*[local-name()='metadata']") + } + $licenseText = Get-ChildText $metadata "license" + if (!$licenseText) { $licenseText = Get-ChildText $metadata "licenseUrl" } + $authorsText = Get-ChildText $metadata "authors" + $copyrightText = Get-ChildText $metadata "copyright" + $projectUrlText = Get-ChildText $metadata "projectUrl" + $license = if ($licenseText) { $licenseText } else { "Not declared in package metadata" } + $authors = if ($authorsText) { $authorsText } else { "Not declared" } + $copyright = if ($copyrightText) { $copyrightText } else { "Not declared" } + $projectUrl = if ($projectUrlText) { $projectUrlText } else { "Not declared" } + + $copied = [Collections.Generic.List[string]]::new() + $noticeFiles = @(Get-ChildItem $package.Root -File -Recurse -ErrorAction SilentlyContinue | Where-Object { + $_.Length -le 5MB -and $_.Name -match '^(license|licence|notice|third[-_. ]?party|thirdparty|copying|copyright)([._ -].*)?$' + }) + foreach ($notice in $noticeFiles) { + $rootPrefix = $package.Root.TrimEnd([char[]]"\/") + $relative = $notice.FullName.Substring($rootPrefix.Length).TrimStart([char[]]"\/") + $targetName = ($relative -replace '[\\/:*?"<>|]', '__') + $target = Join-Path $packageOutput $targetName + Copy-Item $notice.FullName $target -Force + $copied.Add($targetName) + } + if ($copied.Count -eq 0) { Remove-Item $packageOutput -Force } + + $inventory.Add("Package: $($package.Id) $($package.Version)") + $inventory.Add("Authors: $authors") + $inventory.Add("Copyright: $copyright") + $inventory.Add("Licence declaration: $license") + $inventory.Add("Project: $projectUrl") + $inventory.Add("Copied notice files: $(if ($copied.Count) { $copied -join ', ' } else { 'none found in restored package' })") + $inventory.Add("") +} + +[IO.File]::WriteAllLines((Join-Path $OutputRoot "NuGet-LICENCE-INVENTORY.txt"), $inventory, [Text.UTF8Encoding]::new($false)) +Write-Host "Collected legal metadata for $($packages.Count) restored NuGet packages." diff --git a/companion/scripts/generate-companion-icon.ps1 b/companion/scripts/generate-companion-icon.ps1 new file mode 100644 index 0000000..79b6fde --- /dev/null +++ b/companion/scripts/generate-companion-icon.ps1 @@ -0,0 +1,94 @@ +param( + [string]$OutputPath = "" +) + +$ErrorActionPreference = "Stop" +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path +if ([string]::IsNullOrWhiteSpace($OutputPath)) { + $OutputPath = Join-Path $repoRoot "companion\src\Lumi.Companion.App\Assets\Lumi.Companion.ico" +} +$OutputPath = [System.IO.Path]::GetFullPath($OutputPath) +$sizes = @(16, 32, 48, 256) + +function New-LumiIconFrame { + param([int]$Size) + + $radius = [Math]::Max(3, [Math]::Round($Size * 0.22)) + $center = ($Size - 1) / 2.0 + $diamondRadius = $Size * 0.36 + $maskStride = [Math]::Ceiling($Size / 32.0) * 4 + $pixels = New-Object byte[] ($Size * $Size * 4) + + for ($y = 0; $y -lt $Size; $y++) { + for ($x = 0; $x -lt $Size; $x++) { + $inside = $true + if (!(($x -ge $radius -and $x -lt $Size - $radius) -or ($y -ge $radius -and $y -lt $Size - $radius))) { + $cornerX = if ($x -lt $radius) { $radius } else { $Size - $radius - 1 } + $cornerY = if ($y -lt $radius) { $radius } else { $Size - $radius - 1 } + $dx = $x - $cornerX + $dy = $y - $cornerY + $inside = (($dx * $dx) + ($dy * $dy)) -le ($radius * $radius) + } + $diamond = (([Math]::Abs($x - $center) / $diamondRadius) + ([Math]::Abs($y - $center) / $diamondRadius)) -le 1 + $target = ((($Size - 1 - $y) * $Size) + $x) * 4 + $pixels[$target] = if ($diamond -and $inside) { 255 } else { 200 } + $pixels[$target + 1] = if ($diamond -and $inside) { 255 } else { 183 } + $pixels[$target + 2] = if ($diamond -and $inside) { 255 } else { 40 } + $pixels[$target + 3] = if ($inside) { 255 } else { 0 } + } + } + + $stream = [System.IO.MemoryStream]::new() + $writer = [System.IO.BinaryWriter]::new($stream) + $writer.Write([int]40) + $writer.Write([int]$Size) + $writer.Write([int]($Size * 2)) + $writer.Write([uint16]1) + $writer.Write([uint16]32) + $writer.Write([int]0) + $writer.Write([int]$pixels.Length) + $writer.Write([int]0) + $writer.Write([int]0) + $writer.Write([int]0) + $writer.Write([int]0) + $writer.Write($pixels) + $writer.Write((New-Object byte[] ($maskStride * $Size))) + $writer.Flush() + $result = $stream.ToArray() + $writer.Dispose() + $stream.Dispose() + return ,$result +} + +[byte[][]]$frames = @($sizes | ForEach-Object { ,(New-LumiIconFrame -Size $_) }) +$directorySize = 6 + ($frames.Count * 16) +$offset = $directorySize +$output = [System.IO.MemoryStream]::new() +$binary = [System.IO.BinaryWriter]::new($output) +$binary.Write([uint16]0) +$binary.Write([uint16]1) +$binary.Write([uint16]$frames.Count) + +for ($index = 0; $index -lt $frames.Count; $index++) { + $size = $sizes[$index] + $frame = $frames[$index] + $encodedSize = if ($size -eq 256) { 0 } else { $size } + $binary.Write([byte]$encodedSize) + $binary.Write([byte]$encodedSize) + $binary.Write([byte]0) + $binary.Write([byte]0) + $binary.Write([uint16]1) + $binary.Write([uint16]32) + $binary.Write([int]$frame.Length) + $binary.Write([int]$offset) + $offset += $frame.Length +} +foreach ($frame in $frames) { $binary.Write($frame) } +$binary.Flush() + +$directory = Split-Path -Parent $OutputPath +[System.IO.Directory]::CreateDirectory($directory) | Out-Null +[System.IO.File]::WriteAllBytes($OutputPath, $output.ToArray()) +$binary.Dispose() +$output.Dispose() +Write-Host "Generated Lumi Companion icon: $OutputPath" diff --git a/companion/scripts/publish-companion.ps1 b/companion/scripts/publish-companion.ps1 index a74f37d..bc773e8 100644 --- a/companion/scripts/publish-companion.ps1 +++ b/companion/scripts/publish-companion.ps1 @@ -1,6 +1,7 @@ param( - [string]$Version = "0.1.0-experimental.11", - [string]$BridgeVersion = "0.1.0-experimental.5" + [string]$Version = "0.1.0", + [string]$BridgeVersion = "0.1.0", + [string]$ObsVersion = "31.1.1" ) $ErrorActionPreference = "Stop" @@ -9,32 +10,104 @@ $project = Join-Path $repoRoot "companion\src\Lumi.Companion.App\Lumi.Companion. $outputRoot = Join-Path $repoRoot "companion\installer\output" $publishRoot = Join-Path $outputRoot "publish" $stageRoot = Join-Path $outputRoot "package" +$sourceStageRoot = Join-Path $outputRoot "obs-bridge-source" $archive = Join-Path $outputRoot "Lumi.Companion-win-x64.zip" $installer = Join-Path $outputRoot "Lumi.Companion-Setup.exe" +$legalSourceRoot = Join-Path $repoRoot "companion\legal" +$publishLegalRoot = Join-Path $publishRoot "legal" -& (Join-Path $PSScriptRoot "build-obs-bridge.ps1") -BridgeVersion $BridgeVersion -Remove-Item $publishRoot, $stageRoot -Recurse -Force -ErrorAction SilentlyContinue +function Compress-ArchiveWithRetry { + param( + [Parameter(Mandatory = $true)][string]$Path, + [Parameter(Mandatory = $true)][string]$DestinationPath, + [int]$Attempts = 8 + ) + + for ($attempt = 1; $attempt -le $Attempts; $attempt++) { + try { + Remove-Item $DestinationPath -Force -ErrorAction SilentlyContinue + Compress-Archive -Path $Path -DestinationPath $DestinationPath -CompressionLevel Optimal -ErrorAction Stop + return + } catch { + if ($attempt -eq $Attempts) { throw } + Start-Sleep -Seconds 2 + } + } +} + +& (Join-Path $PSScriptRoot "generate-companion-icon.ps1") +& (Join-Path $PSScriptRoot "build-obs-bridge.ps1") -BridgeVersion $BridgeVersion -ObsVersion $ObsVersion +Remove-Item $publishRoot, $stageRoot, $sourceStageRoot -Recurse -Force -ErrorAction SilentlyContinue New-Item -ItemType Directory -Force -Path $publishRoot, $stageRoot | Out-Null $localDotnet = Join-Path $HOME ".dotnet-sdk\dotnet.exe" $dotnet = if (Test-Path $localDotnet) { $localDotnet } else { (Get-Command dotnet.exe -ErrorAction Stop).Source } -$publishArguments = @("publish", "`"$project`"", "-c", "Release", "-r", "win-x64", "--self-contained", "true", "--disable-build-servers", "-o", "`"$publishRoot`"", +$publishArguments = @("publish", $project, "-c", "Release", "-r", "win-x64", "--self-contained", "true", "--disable-build-servers", "-o", $publishRoot, "-p:PublishSingleFile=true", "-p:IncludeNativeLibrariesForSelfExtract=true", "-p:DebugType=None", "-p:Version=$Version") -$published = Start-Process -FilePath $dotnet -ArgumentList $publishArguments -NoNewWindow -Wait -PassThru -if ($published.ExitCode) { throw "Companion publish failed." } +& $dotnet @publishArguments +if ($LASTEXITCODE) { throw "Companion publish failed." } $bridgeDiagnostic = Join-Path $outputRoot "obs-bridge-package-diagnostic.json" Remove-Item $bridgeDiagnostic -Force -ErrorAction SilentlyContinue -$diagnosed = Start-Process -FilePath (Join-Path $publishRoot "Lumi.Companion.App.exe") -ArgumentList @("--diagnose-obs-bridge", "`"$bridgeDiagnostic`"") -Wait -PassThru -if ($diagnosed.ExitCode -ne 0) { +& (Join-Path $publishRoot "Lumi.Companion.App.exe") --diagnose-obs-bridge $bridgeDiagnostic +if ($LASTEXITCODE -ne 0) { $detail = if (Test-Path $bridgeDiagnostic) { Get-Content $bridgeDiagnostic -Raw } else { "No diagnostic result was written." } throw "Published OBS integration payload failed its executable-level check: $detail" } Remove-Item $bridgeDiagnostic -Force -ErrorAction SilentlyContinue +# Copy the release legal bundle beside the application so installed and portable +# builds carry the same terms, privacy notice, and third-party attributions. +New-Item -ItemType Directory -Force -Path $publishLegalRoot | Out-Null +foreach ($name in @( + "LUMI-COMPANION-LICENCE.txt", + "PRIVACY-NOTICE.txt", + "THIRD-PARTY-NOTICES.txt", + "LEGAL-README.txt" +)) { + Copy-Item (Join-Path $legalSourceRoot $name) (Join-Path $publishLegalRoot $name) -Force +} +Copy-Item (Join-Path $legalSourceRoot "third-party") (Join-Path $publishLegalRoot "third-party") -Recurse -Force +& (Join-Path $PSScriptRoot "collect-nuget-notices.ps1") -RepoRoot $repoRoot -OutputRoot (Join-Path $publishLegalRoot "third-party\nuget") + +# The bundled OBS module links with GPL-licensed OBS/libobs. Ship the exact Lumi +# bridge source and build materials with every binary distribution. +$sourceCompanionRoot = Join-Path $sourceStageRoot "companion" +$sourceNativeParent = Join-Path $sourceCompanionRoot "native" +$sourceScriptsRoot = Join-Path $sourceCompanionRoot "scripts" +New-Item -ItemType Directory -Force -Path $sourceNativeParent, $sourceScriptsRoot | Out-Null +Copy-Item (Join-Path $repoRoot "companion\native\obs-bridge") (Join-Path $sourceNativeParent "obs-bridge") -Recurse -Force +Copy-Item (Join-Path $repoRoot "companion\scripts\build-obs-bridge.ps1") (Join-Path $sourceScriptsRoot "build-obs-bridge.ps1") -Force +$upstreamSourceRoot = Join-Path $sourceStageRoot "upstream-source" +New-Item -ItemType Directory -Force -Path $upstreamSourceRoot | Out-Null +$bridgeCacheRoot = Join-Path $env:LOCALAPPDATA "LumiCompanionBuild\obs-$ObsVersion" +$obsSourceArchive = Join-Path $bridgeCacheRoot "obs-source.zip" +$jsonSourceRoot = Join-Path $bridgeCacheRoot "bridge-build\_deps\json-src" +if (!(Test-Path $obsSourceArchive)) { throw "The exact OBS source archive used for the bridge build is missing." } +if (!(Test-Path $jsonSourceRoot)) { throw "The exact nlohmann/json source used for the bridge build is missing." } +Copy-Item $obsSourceArchive (Join-Path $upstreamSourceRoot "OBS-Studio-$ObsVersion-source.zip") -Force +Copy-Item $jsonSourceRoot (Join-Path $upstreamSourceRoot "nlohmann-json-3.12.0") -Recurse -Force +@" +Lumi OBS Bridge corresponding source +Release version: $BridgeVersion + +This archive contains the Lumi-authored source and build script used for the +GPL-2.0-or-later OBS bridge distributed with Lumi Companion. + +The archive includes the Lumi bridge source, its build script, the exact OBS Studio +$ObsVersion source archive used for its interfaces, and the exact nlohmann/json +3.12.0 source used in the binary. Checksums and upstream locations remain recorded +in the build script. OBS Studio binaries are not distributed by Companion. + +Licence: companion/native/obs-bridge/COPYING +"@ | Set-Content -Encoding UTF8 (Join-Path $sourceStageRoot "SOURCE-README.txt") +$bridgeSourceArchive = Join-Path $publishLegalRoot "Lumi-OBS-Bridge-Source.zip" +Compress-ArchiveWithRetry -Path (Join-Path $sourceStageRoot "*") -DestinationPath $bridgeSourceArchive + Copy-Item (Join-Path $publishRoot "Lumi.Companion.App.exe") $stageRoot Copy-Item (Join-Path $publishRoot "components") $stageRoot -Recurse +Copy-Item (Join-Path $publishRoot "legal") $stageRoot -Recurse Remove-Item $archive -Force -ErrorAction SilentlyContinue -Compress-Archive -Path (Join-Path $stageRoot "*") -DestinationPath $archive -CompressionLevel Optimal +Compress-ArchiveWithRetry -Path (Join-Path $stageRoot "*") -DestinationPath $archive $isccCandidates = @( (Join-Path $env:LOCALAPPDATA "Programs\Inno Setup 6\ISCC.exe"), @@ -45,13 +118,13 @@ $iscc = $isccCandidates | Where-Object { $_ -and (Test-Path $_) } | Select-Objec if (-not $iscc) { throw "Inno Setup 6 is required to build the durable per-user Companion installer." } Remove-Item $installer -Force -ErrorAction SilentlyContinue $compileArguments = @( - "`"/DAppVersion=$Version`"", - "`"/DSourceRoot=$publishRoot`"", - "`"/DOutputRoot=$outputRoot`"", - "`"$(Join-Path $repoRoot "companion\installer\Lumi.Companion.iss")`"" + "/DAppVersion=$Version", + "/DSourceRoot=$publishRoot", + "/DOutputRoot=$outputRoot", + (Join-Path $repoRoot "companion\installer\Lumi.Companion.iss") ) -$compiled = Start-Process -FilePath $iscc -ArgumentList $compileArguments -NoNewWindow -Wait -PassThru -if ($compiled.ExitCode -ne 0 -or -not (Test-Path $installer)) { throw "Companion installer compilation failed." } +& $iscc @compileArguments +if ($LASTEXITCODE -ne 0 -or -not (Test-Path $installer)) { throw "Companion installer compilation failed." } $file = Get-Item $archive $sha = (Get-FileHash $archive -Algorithm SHA256).Hash.ToLowerInvariant() diff --git a/companion/scripts/publish-dev-update.ps1 b/companion/scripts/publish-dev-update.ps1 new file mode 100644 index 0000000..5938350 --- /dev/null +++ b/companion/scripts/publish-dev-update.ps1 @@ -0,0 +1,72 @@ +param( + [Parameter(Mandatory = $true)][string]$OutputArchive, + [Parameter(Mandatory = $true)][string]$ManifestPath +) + +$ErrorActionPreference = "Stop" +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path +$project = Join-Path $repoRoot "companion\src\Lumi.Companion.App\Lumi.Companion.App.csproj" +$outputArchive = [System.IO.Path]::GetFullPath($OutputArchive) +$manifest = [System.IO.Path]::GetFullPath($ManifestPath) +$workRoot = Join-Path ([System.IO.Path]::GetDirectoryName($outputArchive)) "work" +$publishRoot = Join-Path $workRoot "publish" +$stageRoot = Join-Path $workRoot "package" + +if (!(Test-Path $manifest)) { throw "The development build manifest is missing." } +& (Join-Path $PSScriptRoot "generate-companion-icon.ps1") +Remove-Item $workRoot -Recurse -Force -ErrorAction SilentlyContinue +New-Item -ItemType Directory -Force -Path $publishRoot, $stageRoot | Out-Null +New-Item -ItemType Directory -Force -Path ([System.IO.Path]::GetDirectoryName($outputArchive)) | Out-Null + +$localDotnet = Join-Path $HOME ".dotnet-sdk\dotnet.exe" +$dotnet = if (Test-Path $localDotnet) { $localDotnet } else { (Get-Command dotnet.exe -ErrorAction Stop).Source } +$arguments = @( + "publish", "`"$project`"", + "-c", "Release", + "-r", "win-x64", + "--self-contained", "true", + "--disable-build-servers", + "-o", "`"$publishRoot`"", + "-p:PublishSingleFile=true", + "-p:IncludeNativeLibrariesForSelfExtract=true", + "-p:DebugType=None" +) +$published = Start-Process -FilePath $dotnet -ArgumentList $arguments -NoNewWindow -Wait -PassThru +if ($published.ExitCode -ne 0) { throw "Companion development publish failed." } + +$app = Join-Path $publishRoot "Lumi.Companion.App.exe" +if (!(Test-Path $app)) { throw "The Companion development publish did not create Lumi.Companion.App.exe." } +Copy-Item $app $stageRoot -Force +$componentsSource = Join-Path $publishRoot "components" +if (Test-Path $componentsSource) { Copy-Item $componentsSource (Join-Path $stageRoot "components") -Recurse -Force } + +$legalSource = Join-Path $repoRoot "companion\legal" +$legalTarget = Join-Path $stageRoot "legal" +if (Test-Path $legalSource) { + New-Item -ItemType Directory -Force -Path $legalTarget | Out-Null + foreach ($name in @("LUMI-COMPANION-LICENCE.txt", "PRIVACY-NOTICE.txt", "THIRD-PARTY-NOTICES.txt", "LEGAL-README.txt")) { + $source = Join-Path $legalSource $name + if (Test-Path $source) { Copy-Item $source (Join-Path $legalTarget $name) -Force } + } + $thirdParty = Join-Path $legalSource "third-party" + if (Test-Path $thirdParty) { Copy-Item $thirdParty (Join-Path $legalTarget "third-party") -Recurse -Force } +} +Copy-Item $manifest (Join-Path $stageRoot ".lumi-dev-build.json") -Force +Remove-Item $outputArchive -Force -ErrorAction SilentlyContinue +$archiveItems = Get-ChildItem $stageRoot -Force | Select-Object -ExpandProperty FullName +$archiveCreated = $false +for ($attempt = 1; $attempt -le 20; $attempt++) { + try { + Compress-Archive -Path $archiveItems -DestinationPath $outputArchive -CompressionLevel Optimal -ErrorAction Stop + $archiveCreated = $true + break + } + catch { + Remove-Item $outputArchive -Force -ErrorAction SilentlyContinue + if ($attempt -eq 20) { throw } + Start-Sleep -Milliseconds 250 + } +} +if (-not $archiveCreated) { throw "The Companion development archive could not be created." } +Remove-Item $workRoot -Recurse -Force -ErrorAction SilentlyContinue +Write-Host "Created localhost development update: $outputArchive" diff --git a/companion/scripts/verify-song-overlay.ps1 b/companion/scripts/verify-song-overlay.ps1 new file mode 100644 index 0000000..56b3640 --- /dev/null +++ b/companion/scripts/verify-song-overlay.ps1 @@ -0,0 +1,15 @@ +$ErrorActionPreference = 'Stop' +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path +Push-Location $repoRoot +try { + Write-Host 'Building Lumi Companion and all registered Companion plugins...' + dotnet build 'companion/Lumi.Companion.sln' -c Release -p:EnableWindowsTargeting=true + + Write-Host 'Verifying the existing Lumi Song Overlay server plugin...' + node 'plugins/now_playing/tests/verify.js' + + Write-Host 'Song Overlay Companion verification passed.' -ForegroundColor Green +} +finally { + Pop-Location +} diff --git a/companion/src/Lumi.Companion.Abstractions/CompanionPluginTransport.cs b/companion/src/Lumi.Companion.Abstractions/CompanionPluginTransport.cs new file mode 100644 index 0000000..1f5950e --- /dev/null +++ b/companion/src/Lumi.Companion.Abstractions/CompanionPluginTransport.cs @@ -0,0 +1,22 @@ +namespace Lumi.Companion.Abstractions; + +public sealed record CompanionPluginHttpResponse( + int StatusCode, + string Body) +{ + public bool IsSuccessStatusCode => StatusCode is >= 200 and <= 299; +} + +/// +/// A plugin-scoped view of the Companion shell's authenticated Lumi transport. +/// Plugins never receive, store, or refresh the paired device credential. +/// +public interface ICompanionPluginTransport +{ + Uri? LumiBaseUri { get; } + bool IsAuthenticated { get; } + Task PostJsonAsync( + string relativePath, + object payload, + CancellationToken cancellationToken = default); +} diff --git a/companion/src/Lumi.Companion.Abstractions/Lumi.Companion.Abstractions.csproj b/companion/src/Lumi.Companion.Abstractions/Lumi.Companion.Abstractions.csproj new file mode 100644 index 0000000..bfa77a1 --- /dev/null +++ b/companion/src/Lumi.Companion.Abstractions/Lumi.Companion.Abstractions.csproj @@ -0,0 +1,7 @@ + + + net8.0 + enable + enable + + diff --git a/companion/src/Lumi.Companion.Abstractions/PluginContributions.cs b/companion/src/Lumi.Companion.Abstractions/PluginContributions.cs new file mode 100644 index 0000000..e1b6fa7 --- /dev/null +++ b/companion/src/Lumi.Companion.Abstractions/PluginContributions.cs @@ -0,0 +1,47 @@ +namespace Lumi.Companion.Abstractions; + +public enum CompanionPluginHealth +{ + Ready, + Healthy, + Warning, + Error +} + +public sealed record CompanionPluginDescriptor( + string Id, + string Name, + Version Version, + string Description, + int Order = 100); + +public sealed record CompanionPluginPage( + string Key, + string Label, + int Order = 100); + +public sealed record CompanionPluginStatus( + CompanionPluginHealth Health, + string Summary, + string? Detail = null); + +public sealed record CompanionPluginAction( + string Id, + Func Label, + Func Execute, + Func? CanExecute = null, + int Order = 100); + +/// +/// A UI-neutral contribution contract. The Avalonia shell owns rendering and +/// guarantees that each plugin receives one nested root in both the sidebar and +/// tray menu instead of leaking loose actions into the global navigation. +/// +public interface ICompanionPluginContribution +{ + CompanionPluginDescriptor Descriptor { get; } + IReadOnlyList Pages { get; } + IReadOnlyList Actions { get; } + CompanionPluginStatus Status { get; } + event Action? Changed; +} diff --git a/companion/src/Lumi.Companion.App/App.axaml b/companion/src/Lumi.Companion.App/App.axaml index e638259..1121586 100644 --- a/companion/src/Lumi.Companion.App/App.axaml +++ b/companion/src/Lumi.Companion.App/App.axaml @@ -73,6 +73,10 @@ +