@hraness/ghostget 0.17.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +458 -0
- package/LICENSE +21 -0
- package/README.md +1359 -0
- package/bunfig.toml +2 -0
- package/dist/apple-photos-client.js +704 -0
- package/dist/beeper-client.js +4602 -0
- package/dist/client.js +1605 -0
- package/dist/index-26yq8q16.js +31 -0
- package/dist/index-9t13b4k7.js +508 -0
- package/dist/index-axwm9b3v.js +5 -0
- package/dist/index-gwk7rbyj.js +30 -0
- package/dist/index.js +701 -0
- package/dist/messaging.js +1130 -0
- package/dist/omni-client.js +1406 -0
- package/dist/whatsapp-client.js +355 -0
- package/docs/imessage-direct-provider.md +106 -0
- package/docs/rental-listings.md +94 -0
- package/package.json +600 -0
- package/skills/ghostget/SKILL.md +251 -0
- package/skills/ghostget/agents/openai.yaml +4 -0
- package/skills/ghostget/references/adapter-contract.md +181 -0
- package/skills/ghostget/references/apple-photos.md +115 -0
- package/skills/ghostget/references/article-drafts.md +235 -0
- package/skills/ghostget/references/code-owned-provider-scaffold.md +134 -0
- package/skills/ghostget/references/cross-posting-video.md +139 -0
- package/skills/ghostget/references/cross-posting.md +82 -0
- package/skills/ghostget/references/derivation.md +390 -0
- package/skills/ghostget/references/hraness-social-profile-stats.json +269 -0
- package/skills/ghostget/references/install.md +66 -0
- package/skills/ghostget/references/linkedin-adapter.md +442 -0
- package/skills/ghostget/references/messaging.md +168 -0
- package/skills/ghostget/references/meta-comet-contract.md +153 -0
- package/skills/ghostget/references/platform-patterns.md +122 -0
- package/skills/ghostget/references/provider-plugins.md +496 -0
- package/skills/ghostget/references/reddit-flair.md +78 -0
- package/skills/ghostget/references/safety-and-state.md +214 -0
- package/skills/ghostget/references/settlement-and-duplicate-cleanup.md +79 -0
- package/skills/ghostget/references/social-platform-routing.md +66 -0
- package/skills/ghostget/references/social-profile-stats.md +195 -0
- package/skills/ghostget/references/social-video-platform-routing.md +89 -0
- package/skills/ghostget/references/whatsapp-adapter.md +101 -0
- package/skills/ghostget/references/x-adapter.md +332 -0
- package/skills/ghostget/references/x-ai-disclosure.md +71 -0
- package/src/apple-photos-cli.ts +639 -0
- package/src/apple-photos-client-types.ts +168 -0
- package/src/apple-photos-client.ts +201 -0
- package/src/apple-photos-contact-evidence.ts +838 -0
- package/src/apple-photos-local-source.ts +1131 -0
- package/src/args.ts +2053 -0
- package/src/article-draft-document.ts +417 -0
- package/src/article-draft-embeds.ts +123 -0
- package/src/article-draft-images.ts +214 -0
- package/src/assets/adapter-template/wrench-adapter.json +40 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.json +1500 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v1.0.0.json +129 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v1.1.0.json +207 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v2.0.0.json +1463 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v2.1.0.json +1463 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v2.2.0.json +1475 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v2.3.0.json +1488 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.json +684 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.0.0.json +581 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.1.0.json +581 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.2.0.json +581 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.3.0.json +615 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.4.0.json +684 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.5.0.json +684 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.6.0.json +684 -0
- package/src/assets/adapters/clasificados/wrench-web-adapter.json +54 -0
- package/src/assets/adapters/facebook/wrench-web-adapter.json +750 -0
- package/src/assets/adapters/facebook/wrench-web-adapter.v1.0.0.json +706 -0
- package/src/assets/adapters/facebook/wrench-web-adapter.v1.1.0.json +703 -0
- package/src/assets/adapters/facebook/wrench-web-adapter.v1.2.0.json +750 -0
- package/src/assets/adapters/facebook-group/wrench-web-adapter.json +682 -0
- package/src/assets/adapters/facebook-group/wrench-web-adapter.v1.0.0.json +583 -0
- package/src/assets/adapters/facebook-group/wrench-web-adapter.v1.1.0.json +682 -0
- package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.json +437 -0
- package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.0.0.json +383 -0
- package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.1.0.json +437 -0
- package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.1.1.json +437 -0
- package/src/assets/adapters/facebook-page/wrench-web-adapter.json +880 -0
- package/src/assets/adapters/facebook-page/wrench-web-adapter.v1.0.0.json +740 -0
- package/src/assets/adapters/facebook-page/wrench-web-adapter.v1.1.0.json +880 -0
- package/src/assets/adapters/github/wrench-web-adapter.json +69 -0
- package/src/assets/adapters/github/wrench-web-adapter.v1.0.0.json +42 -0
- package/src/assets/adapters/github/wrench-web-adapter.v1.1.0.json +69 -0
- package/src/assets/adapters/gmail/wrench-adapter.json +161 -0
- package/src/assets/adapters/gmail/wrench-adapter.v1.2.0.json +157 -0
- package/src/assets/adapters/hacker-news/wrench-web-adapter.json +315 -0
- package/src/assets/adapters/hacker-news/wrench-web-adapter.v1.0.0.json +315 -0
- package/src/assets/adapters/imessage/wrench-web-adapter.json +236 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.json +719 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.0.0.json +627 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.1.0.json +609 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.2.0.json +640 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.3.0.json +671 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.4.0.json +710 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.5.0.json +710 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.6.0.json +719 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.7.0.json +719 -0
- package/src/assets/adapters/linkedin/wrench-adapter.json +442 -0
- package/src/assets/adapters/linkedin/wrench-adapter.v0.4.0.json +124 -0
- package/src/assets/adapters/linkedin/wrench-adapter.v1.0.0.json +411 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.json +914 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.0.0.json +604 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.1.0.json +604 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.10.0.json +809 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.11.0.json +809 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.12.0.json +820 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.13.0.json +819 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.14.0.json +819 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.15.0.json +819 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.16.0.json +874 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.17.0.json +874 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.18.0.json +874 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.19.0.json +874 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.2.0.json +731 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.3.0.json +768 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.4.0.json +808 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.5.0.json +808 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.6.0.json +808 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.7.0.json +825 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.8.0.json +792 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.9.0.json +809 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.json +855 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.0.0.json +577 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.1.0.json +604 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.10.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.11.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.12.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.2.0.json +653 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.3.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.4.0.json +713 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.5.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.6.0.json +713 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.7.0.json +713 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.8.0.json +713 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.9.0.json +713 -0
- package/src/assets/adapters/substack/wrench-web-adapter.json +936 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.0.0.json +817 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.1.0.json +812 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.2.0.json +812 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.3.0.json +902 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.4.0.json +936 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.5.0.json +936 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.6.0.json +936 -0
- package/src/assets/adapters/threads/wrench-web-adapter.json +692 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.0.0.json +629 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.1.0.json +623 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.2.0.json +622 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.3.0.json +622 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.4.0.json +622 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.5.0.json +699 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.6.0.json +693 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.7.0.json +692 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.json +762 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.v1.0.0.json +681 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.v1.1.0.json +708 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.v1.2.0.json +762 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.v1.3.0.json +762 -0
- package/src/assets/adapters/twitch/wrench-web-adapter.json +42 -0
- package/src/assets/adapters/twitch/wrench-web-adapter.v1.0.0.json +42 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.json +400 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.0.0.json +355 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.1.0.json +386 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.2.0.json +386 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.3.0.json +400 -0
- package/src/assets/adapters/x/wrench-adapter.json +749 -0
- package/src/assets/adapters/x/wrench-adapter.v1.0.0.json +700 -0
- package/src/assets/adapters/x/wrench-adapter.v1.1.0.json +707 -0
- package/src/assets/adapters/x/wrench-web-adapter.json +712 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.1.0.json +579 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.10.0.json +685 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.11.0.json +712 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.12.0.json +712 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.13.0.json +712 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.2.0.json +578 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.3.0.json +610 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.4.0.json +617 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.5.0.json +634 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.6.0.json +632 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.7.0.json +649 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.8.0.json +649 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.9.0.json +683 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.json +561 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.v1.0.0.json +465 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.v1.1.0.json +519 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.v1.2.0.json +561 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.v1.3.0.json +561 -0
- package/src/assets/code-owned-provider-template/plugin.test.ts.template +54 -0
- package/src/assets/code-owned-provider-template/plugin.ts.template +102 -0
- package/src/assets/code-owned-provider-template/runtime.internal.test.ts.template +44 -0
- package/src/assets/code-owned-provider-template/runtime.ts.template +59 -0
- package/src/assets/code-owned-provider-template/wrench-adapter.json.template +33 -0
- package/src/assets/fonts/nebula-sans/LICENSE.txt +96 -0
- package/src/assets/fonts/nebula-sans/NebulaSans-Book.woff2 +0 -0
- package/src/assets/fonts/nebula-sans/PROVENANCE.md +14 -0
- package/src/assets.d.ts +4 -0
- package/src/auth.ts +1157 -0
- package/src/beeper-client-types.ts +169 -0
- package/src/beeper-client.ts +250 -0
- package/src/beeper-contact-interactions-cli.ts +180 -0
- package/src/beeper-contact-interactions.ts +1612 -0
- package/src/beeper-message-bundle-v1.ts +257 -0
- package/src/beeper-message-like-me-cli.ts +84 -0
- package/src/beeper-message-like-me-export.ts +2274 -0
- package/src/beeper-message-like-me-recovery.ts +931 -0
- package/src/beeper-message-like-me-source.ts +4034 -0
- package/src/browser-admission.ts +928 -0
- package/src/browser-snapshots.ts +260 -0
- package/src/browser.ts +3873 -0
- package/src/canonical-json.ts +60 -0
- package/src/catalog-cli.ts +638 -0
- package/src/cli.ts +208 -0
- package/src/client-types.ts +301 -0
- package/src/client.ts +2726 -0
- package/src/confirmed-write-failure.ts +44 -0
- package/src/confirmed-write-model.ts +72 -0
- package/src/confirmed-write-platform.ts +1055 -0
- package/src/confirmed-write-program.ts +224 -0
- package/src/confirmed-write-runtime.ts +16 -0
- package/src/cursor-token.ts +228 -0
- package/src/derivation-dnr-readiness.ts +367 -0
- package/src/derivation-file-chooser.ts +384 -0
- package/src/derivation-network-boundary.ts +835 -0
- package/src/derivation-network-guard.ts +782 -0
- package/src/derivation-network-proxy-helper.ts +440 -0
- package/src/derivation-network-proxy.ts +385 -0
- package/src/derive-command-helper.ts +693 -0
- package/src/derive-fixtures.ts +343 -0
- package/src/derive-review.ts +980 -0
- package/src/derive.ts +4754 -0
- package/src/fixtures/beeper-message-like-me-v1/accounts.ndjson +2 -0
- package/src/fixtures/beeper-message-like-me-v1/conversations.ndjson +1 -0
- package/src/fixtures/beeper-message-like-me-v1/manifest.json +1 -0
- package/src/fixtures/beeper-message-like-me-v1/messages.ndjson +2 -0
- package/src/fixtures/beeper-message-like-me-v1/participants.ndjson +3 -0
- package/src/fixtures/beeper-message-like-me-v1/reactions.ndjson +1 -0
- package/src/fixtures/beeper-message-like-me-v1/tombstones.ndjson +1 -0
- package/src/ghostget.ts +3672 -0
- package/src/gmail-capture.ts +1244 -0
- package/src/har-internal.ts +1242 -0
- package/src/har.ts +743 -0
- package/src/index.ts +218 -0
- package/src/invocation-read-platform.ts +55 -0
- package/src/invocation-read-program.ts +93 -0
- package/src/invocation-read-runtime.ts +11 -0
- package/src/linked-device-lifecycle-admission.ts +450 -0
- package/src/linked-device-lifecycle-journal.ts +1560 -0
- package/src/linked-device-lifecycle-runtime.ts +1602 -0
- package/src/local-cli-admission.ts +135 -0
- package/src/local-cli-contracts.ts +271 -0
- package/src/local-cli-execution.ts +98 -0
- package/src/local-cli-surface-contract.ts +1510 -0
- package/src/local-cli-tool-identity.ts +336 -0
- package/src/media/archive.ts +2325 -0
- package/src/media/args.ts +303 -0
- package/src/media/cli.ts +287 -0
- package/src/media/doctor.ts +475 -0
- package/src/media/ffmpeg.ts +1330 -0
- package/src/media/http-capture.ts +711 -0
- package/src/media/http-probe.ts +593 -0
- package/src/media/http.ts +387 -0
- package/src/media/index.ts +10 -0
- package/src/media/local-transcription.ts +613 -0
- package/src/media/lock.ts +321 -0
- package/src/media/manifest.ts +1712 -0
- package/src/media/metadata.ts +906 -0
- package/src/media/process.ts +723 -0
- package/src/media/revision.ts +166 -0
- package/src/media/runtime-closure.ts +1063 -0
- package/src/media/source-router.ts +187 -0
- package/src/media/transcriber-config.ts +1122 -0
- package/src/media/transcript-persistence-model.ts +15 -0
- package/src/media/transcript-persistence-platform.ts +89 -0
- package/src/media/transcript-persistence-program.ts +39 -0
- package/src/media/transcript-persistence-runtime.ts +24 -0
- package/src/media/transcript.ts +616 -0
- package/src/media/utf8-order.ts +30 -0
- package/src/media/whisper-cpp.ts +464 -0
- package/src/media/whisper-language.ts +58 -0
- package/src/media/yt-dlp.ts +439 -0
- package/src/message-like-me-agentic-messaging.ts +882 -0
- package/src/messaging-action-store.ts +793 -0
- package/src/messaging-runtime.ts +2136 -0
- package/src/messaging-store.ts +539 -0
- package/src/messaging-types.ts +1663 -0
- package/src/messaging.ts +334 -0
- package/src/model.ts +2671 -0
- package/src/oauth-google.ts +888 -0
- package/src/omni-client-types.ts +306 -0
- package/src/omni-client.ts +2225 -0
- package/src/omni-limits.ts +34 -0
- package/src/omni-model.ts +1614 -0
- package/src/omni-request.ts +379 -0
- package/src/omni-runtime.ts +1632 -0
- package/src/operation-deadline.ts +189 -0
- package/src/path-helper.ts +1656 -0
- package/src/pinned-https.ts +194 -0
- package/src/plan-assets.ts +832 -0
- package/src/platform-catalog.ts +1832 -0
- package/src/plugins/beeper-linked-device/plugin.ts +310 -0
- package/src/plugins/bluesky-web/plugin.ts +178 -0
- package/src/plugins/clasificados-web/plugin.ts +66 -0
- package/src/plugins/github-web/plugin.ts +80 -0
- package/src/plugins/gmail-official/plugin.ts +110 -0
- package/src/plugins/hacker-news-web/plugin.ts +52 -0
- package/src/plugins/imessage-direct/plugin.ts +195 -0
- package/src/plugins/imessage-direct/vendor/0001-fix-keep-AppleScript-send-payloads-out-of-child-argv.patch +1430 -0
- package/src/plugins/imessage-direct/vendor/0002-feat-rpc-add-exact-chat-lookup.patch +225 -0
- package/src/plugins/imessage-direct/vendor/provenance.json +80 -0
- package/src/plugins/linkedin-official/plugin.ts +84 -0
- package/src/plugins/linkedin-web/plugin.ts +412 -0
- package/src/plugins/meta-web/plugin.ts +317 -0
- package/src/plugins/reddit-web/flair.ts +492 -0
- package/src/plugins/reddit-web/plugin.ts +477 -0
- package/src/plugins/substack-web/plugin.ts +124 -0
- package/src/plugins/tiktok-web/plugin.ts +67 -0
- package/src/plugins/twitch-web/plugin.ts +54 -0
- package/src/plugins/whatsapp-linked-device/plugin.ts +105 -0
- package/src/plugins/x-official/plugin.ts +64 -0
- package/src/plugins/x-web/plugin.ts +488 -0
- package/src/plugins/youtube-web/plugin.ts +101 -0
- package/src/portable-run-recovery.ts +466 -0
- package/src/process-identity.ts +501 -0
- package/src/profile-clone-helper.ts +104 -0
- package/src/provider-catalog-views.ts +51 -0
- package/src/provider-context.ts +69 -0
- package/src/provider-contract-definitions-x.ts +45 -0
- package/src/provider-contract-definitions.ts +948 -0
- package/src/provider-contract-input-gmail.ts +176 -0
- package/src/provider-contract-input-linkedin.ts +77 -0
- package/src/provider-contract-input-x.ts +304 -0
- package/src/provider-contract-planning.ts +23 -0
- package/src/provider-contract-semantic-identity.ts +54 -0
- package/src/provider-contracts.ts +251 -0
- package/src/provider-http.ts +723 -0
- package/src/provider-plugin-auth.ts +36 -0
- package/src/provider-plugin-builtins.ts +229 -0
- package/src/provider-plugin-cleanup-barrier.ts +229 -0
- package/src/provider-plugin-cleanup-execution.ts +79 -0
- package/src/provider-plugin-cleanup-resource.ts +470 -0
- package/src/provider-plugin-contract-identity.ts +707 -0
- package/src/provider-plugin-host.ts +1972 -0
- package/src/provider-plugin-identifiers.ts +60 -0
- package/src/provider-plugin-invocation-lease.ts +889 -0
- package/src/provider-plugin-lifecycle-kernel.ts +566 -0
- package/src/provider-plugin-lifecycle.ts +1455 -0
- package/src/provider-plugin-module-analysis.ts +336 -0
- package/src/provider-plugin-package.ts +2530 -0
- package/src/provider-plugin-portable-authority.ts +106 -0
- package/src/provider-plugin-portable-catalog.ts +94 -0
- package/src/provider-plugin-portable-identity.ts +315 -0
- package/src/provider-plugin-portable-runtime.ts +2057 -0
- package/src/provider-plugin-protocol.ts +1625 -0
- package/src/provider-plugin-registry.ts +3342 -0
- package/src/provider-plugin-store.ts +2155 -0
- package/src/provider-plugin-test-fixture.ts +5 -0
- package/src/provider-plugin.ts +4907 -0
- package/src/provider-plugins.generated.ts +43 -0
- package/src/provider-plugins.ts +8 -0
- package/src/provider-subject.ts +41 -0
- package/src/provider.ts +315 -0
- package/src/providers/beeper-local-runtime.ts +5426 -0
- package/src/providers/beeper-local.ts +2739 -0
- package/src/providers/beeper-messaging.ts +462 -0
- package/src/providers/beeper-omni.ts +1159 -0
- package/src/providers/bluesky-web-runtime.ts +2895 -0
- package/src/providers/bluesky-web.ts +1614 -0
- package/src/providers/clasificados-web-runtime.ts +255 -0
- package/src/providers/clasificados-web.ts +503 -0
- package/src/providers/contact-projection.ts +301 -0
- package/src/providers/github-read-model.ts +94 -0
- package/src/providers/github-read-platform.ts +165 -0
- package/src/providers/github-read-program.ts +97 -0
- package/src/providers/github-web-runtime.ts +320 -0
- package/src/providers/github-web.ts +403 -0
- package/src/providers/gmail-api.ts +2834 -0
- package/src/providers/gmail-omni.ts +830 -0
- package/src/providers/gmail.ts +1130 -0
- package/src/providers/hacker-news-web-runtime.ts +220 -0
- package/src/providers/hacker-news-web.ts +809 -0
- package/src/providers/imessage-direct-install.ts +354 -0
- package/src/providers/imessage-direct-messaging.ts +532 -0
- package/src/providers/imessage-direct-omni.ts +400 -0
- package/src/providers/imessage-direct-runtime.ts +1498 -0
- package/src/providers/imessage-direct.ts +351 -0
- package/src/providers/instagram-video-foundations.ts +913 -0
- package/src/providers/instagram-web-profile-browser.ts +600 -0
- package/src/providers/iso-bmff.ts +338 -0
- package/src/providers/linkedin-company-platform.ts +42 -0
- package/src/providers/linkedin-company-program.ts +88 -0
- package/src/providers/linkedin-contact-failure.ts +84 -0
- package/src/providers/linkedin-contact-platform.ts +45 -0
- package/src/providers/linkedin-contact-program.ts +91 -0
- package/src/providers/linkedin-profile-activity-failure.ts +76 -0
- package/src/providers/linkedin-profile-activity-platform.ts +41 -0
- package/src/providers/linkedin-profile-activity-program.ts +67 -0
- package/src/providers/linkedin-read-failure.ts +52 -0
- package/src/providers/linkedin-self-platform.ts +53 -0
- package/src/providers/linkedin-self-program.ts +122 -0
- package/src/providers/linkedin-web-article-browser.ts +794 -0
- package/src/providers/linkedin-web-bootstrap.ts +146 -0
- package/src/providers/linkedin-web-contact.ts +830 -0
- package/src/providers/linkedin-web-feed-browser.ts +832 -0
- package/src/providers/linkedin-web-feed.ts +864 -0
- package/src/providers/linkedin-web-post-browser.ts +653 -0
- package/src/providers/linkedin-web-profile-browser.ts +683 -0
- package/src/providers/linkedin-web-runtime.ts +2388 -0
- package/src/providers/linkedin-web.ts +3305 -0
- package/src/providers/linkedin.ts +1885 -0
- package/src/providers/meta-bootstrap.ts +1043 -0
- package/src/providers/meta-facebook-group.ts +690 -0
- package/src/providers/meta-marketplace-relay.ts +747 -0
- package/src/providers/meta-omni.ts +373 -0
- package/src/providers/meta-relay-bundle.ts +1049 -0
- package/src/providers/meta-web-descriptors.ts +1740 -0
- package/src/providers/meta-web-runtime.ts +3388 -0
- package/src/providers/meta-web.ts +2931 -0
- package/src/providers/read-failure.ts +139 -0
- package/src/providers/reddit-omni.ts +462 -0
- package/src/providers/reddit-web-runtime.ts +1929 -0
- package/src/providers/reddit-web.ts +1611 -0
- package/src/providers/rental-listings.ts +460 -0
- package/src/providers/substack-omni.ts +406 -0
- package/src/providers/substack-video-mp4.ts +33 -0
- package/src/providers/substack-web-runtime.ts +2812 -0
- package/src/providers/substack-web.ts +1502 -0
- package/src/providers/tiktok-video-mp4.ts +33 -0
- package/src/providers/tiktok-web-runtime.ts +770 -0
- package/src/providers/tiktok-web.ts +2786 -0
- package/src/providers/twitch-web-runtime.ts +226 -0
- package/src/providers/twitch-web.ts +274 -0
- package/src/providers/whatsapp-account-identity.ts +55 -0
- package/src/providers/whatsapp-contact-projection-helper.ts +874 -0
- package/src/providers/whatsapp-contact-projection-protocol.ts +403 -0
- package/src/providers/whatsapp-interaction-projection-helper.ts +1452 -0
- package/src/providers/whatsapp-interaction-projection-protocol.ts +462 -0
- package/src/providers/whatsapp-message-export-projection-protocol.ts +532 -0
- package/src/providers/whatsapp-messaging.ts +132 -0
- package/src/providers/whatsapp-omni.ts +586 -0
- package/src/providers/whatsapp-web-runtime.ts +3108 -0
- package/src/providers/whatsapp-web.ts +1037 -0
- package/src/providers/x-image-provenance.ts +523 -0
- package/src/providers/x-made-with-ai.ts +106 -0
- package/src/providers/x-transaction-id.ts +413 -0
- package/src/providers/x-web-runtime.ts +3485 -0
- package/src/providers/x-web.ts +2332 -0
- package/src/providers/x.ts +1889 -0
- package/src/providers/youtube-web-runtime.ts +1400 -0
- package/src/providers/youtube-web.ts +1221 -0
- package/src/read-admission-runtime.ts +91 -0
- package/src/read-client.ts +521 -0
- package/src/read-effect-platform.ts +7 -0
- package/src/read-effect-runtime.ts +14 -0
- package/src/read-effect.ts +52 -0
- package/src/read-projection-admission.ts +866 -0
- package/src/read-projections.ts +3151 -0
- package/src/recovery.ts +1492 -0
- package/src/reviewed-template.ts +146 -0
- package/src/run-journal.ts +1272 -0
- package/src/runtime.ts +5116 -0
- package/src/scripts/generate-provider-plugin-catalog.ts +121 -0
- package/src/scripts/install-whatsapp-protocol.sh +232 -0
- package/src/scripts/recover-install-stages.ts +1491 -0
- package/src/scripts/resolve-state-home.ts +30 -0
- package/src/scripts/scaffold-web-provider.ts +973 -0
- package/src/scripts/sync-bundled-adapters.ts +695 -0
- package/src/session-secrets.ts +1183 -0
- package/src/state-helper.bunfig.toml +2 -0
- package/src/state-helper.ts +2914 -0
- package/src/storage.ts +3842 -0
- package/src/transport-policy.ts +2 -0
- package/src/usage.ts +182 -0
- package/src/version.ts +2 -0
- package/src/web-session-authentication-policy.ts +136 -0
- package/src/web-session-cleanup-admission.ts +2344 -0
- package/src/web-session-cleanup-join.ts +99 -0
- package/src/web-session-client.ts +996 -0
- package/src/web-session-contract-definitions.ts +820 -0
- package/src/web-session-contract-planning.ts +42 -0
- package/src/web-session-contracts.ts +228 -0
- package/src/web-session-cookies.ts +500 -0
- package/src/web-session-execution.ts +385 -0
- package/src/web-session-http.ts +373 -0
- package/src/web-session-read-errors.ts +19 -0
- package/src/web-session-read-runtime.ts +124 -0
- package/src/web-session-recovery.ts +818 -0
- package/src/web-session-sites.ts +58 -0
- package/src/web-session-template.ts +994 -0
- package/src/web-session.ts +93 -0
- package/src/whatsapp-client-binding.ts +21 -0
- package/src/whatsapp-client-types.ts +75 -0
- package/src/whatsapp-client.ts +395 -0
- package/src/whatsapp-export-coordinate.ts +17 -0
- package/src/whatsapp-message-bundle-v2.ts +195 -0
- package/src/whatsapp-message-like-me-cli.ts +195 -0
- package/src/whatsapp-message-like-me-export.ts +1072 -0
- package/src/whatsapp-message-like-me-source.ts +1012 -0
- package/tsconfig.json +67 -0
|
@@ -0,0 +1,1330 @@
|
|
|
1
|
+
import { constants, type BigIntStats } from "node:fs";
|
|
2
|
+
import { link, lstat, open, unlink } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
runProcess,
|
|
7
|
+
type CommandArgv,
|
|
8
|
+
type ProcessResult,
|
|
9
|
+
type RunProcessOptions,
|
|
10
|
+
} from "./process";
|
|
11
|
+
|
|
12
|
+
const DEFAULT_PROBE_TIMEOUT_MS = 60_000;
|
|
13
|
+
const DEFAULT_REMUX_TIMEOUT_MS = 2 * 60 * 60 * 1_000;
|
|
14
|
+
const PROBE_STDOUT_LIMIT_BYTES = 4 * 1024 * 1024;
|
|
15
|
+
const TOOL_STDERR_LIMIT_BYTES = 1024 * 1024;
|
|
16
|
+
const REMUX_STDOUT_LIMIT_BYTES = 64 * 1024;
|
|
17
|
+
const MAX_FFPROBE_JSON_CODE_UNITS = 4 * 1024 * 1024;
|
|
18
|
+
const MAX_INSPECTED_STREAMS = 16_384;
|
|
19
|
+
|
|
20
|
+
export const PCM_NORMALIZATION_PROFILE = "pcm-s16le-16000hz-mono-v1" as const;
|
|
21
|
+
export const PCM_NORMALIZATION_TIMEOUT_MS = 2 * 60 * 60 * 1_000;
|
|
22
|
+
export const PCM_NORMALIZATION_MAX_BYTES = (4 * 1024 * 1024 * 1024) - 1;
|
|
23
|
+
const PCM_WAVE_HEADER_BYTES = 44;
|
|
24
|
+
const PCM_SAMPLE_RATE = 16_000;
|
|
25
|
+
const PCM_CHANNELS = 1;
|
|
26
|
+
const PCM_BITS_PER_SAMPLE = 16;
|
|
27
|
+
const PCM_BLOCK_ALIGN = 2;
|
|
28
|
+
const PCM_BYTES_PER_SECOND = PCM_SAMPLE_RATE * PCM_BLOCK_ALIGN;
|
|
29
|
+
|
|
30
|
+
export type MediaStreamKind =
|
|
31
|
+
| "video"
|
|
32
|
+
| "audio"
|
|
33
|
+
| "subtitle"
|
|
34
|
+
| "data"
|
|
35
|
+
| "attachment"
|
|
36
|
+
| "other";
|
|
37
|
+
|
|
38
|
+
export type InspectedMediaStream = Readonly<{
|
|
39
|
+
index: number;
|
|
40
|
+
kind: MediaStreamKind;
|
|
41
|
+
codecName: string | null;
|
|
42
|
+
}>;
|
|
43
|
+
|
|
44
|
+
export type MediaInspection = Readonly<{
|
|
45
|
+
streams: readonly InspectedMediaStream[];
|
|
46
|
+
hasVideo: boolean;
|
|
47
|
+
hasAudio: boolean;
|
|
48
|
+
firstVideoStreamIndex: number | null;
|
|
49
|
+
firstAudioStreamIndex: number | null;
|
|
50
|
+
}>;
|
|
51
|
+
|
|
52
|
+
export type FfprobeParseErrorCode =
|
|
53
|
+
| "invalid-json"
|
|
54
|
+
| "invalid-root"
|
|
55
|
+
| "invalid-streams"
|
|
56
|
+
| "too-many-streams"
|
|
57
|
+
| "invalid-stream";
|
|
58
|
+
|
|
59
|
+
export type FfprobeParseResult =
|
|
60
|
+
| Readonly<{
|
|
61
|
+
ok: true;
|
|
62
|
+
inspection: MediaInspection;
|
|
63
|
+
}>
|
|
64
|
+
| Readonly<{
|
|
65
|
+
ok: false;
|
|
66
|
+
error: Readonly<{
|
|
67
|
+
code: FfprobeParseErrorCode;
|
|
68
|
+
message: string;
|
|
69
|
+
}>;
|
|
70
|
+
}>;
|
|
71
|
+
|
|
72
|
+
export type MediaProbeFailureReason = "process" | "runner" | "invalid-output";
|
|
73
|
+
|
|
74
|
+
export type MediaProbeResult =
|
|
75
|
+
| Readonly<{
|
|
76
|
+
ok: true;
|
|
77
|
+
inspection: MediaInspection;
|
|
78
|
+
}>
|
|
79
|
+
| Readonly<{
|
|
80
|
+
ok: false;
|
|
81
|
+
reason: MediaProbeFailureReason;
|
|
82
|
+
diagnostic: string;
|
|
83
|
+
}>;
|
|
84
|
+
|
|
85
|
+
export type MediaDerivativeRole = "video" | "audio";
|
|
86
|
+
|
|
87
|
+
type DerivativeResultBase = Readonly<{
|
|
88
|
+
role: MediaDerivativeRole;
|
|
89
|
+
path: string;
|
|
90
|
+
}>;
|
|
91
|
+
|
|
92
|
+
export type MediaDerivativeResult =
|
|
93
|
+
| (DerivativeResultBase &
|
|
94
|
+
Readonly<{
|
|
95
|
+
status: "created";
|
|
96
|
+
sourceStreamIndex: number;
|
|
97
|
+
}>)
|
|
98
|
+
| (DerivativeResultBase &
|
|
99
|
+
Readonly<{
|
|
100
|
+
status: "not-present";
|
|
101
|
+
}>)
|
|
102
|
+
| (DerivativeResultBase &
|
|
103
|
+
Readonly<{
|
|
104
|
+
status: "not-requested";
|
|
105
|
+
}>)
|
|
106
|
+
| (DerivativeResultBase &
|
|
107
|
+
Readonly<{
|
|
108
|
+
status: "exists";
|
|
109
|
+
sourceStreamIndex: number;
|
|
110
|
+
}>)
|
|
111
|
+
| (DerivativeResultBase &
|
|
112
|
+
Readonly<{
|
|
113
|
+
status: "failed";
|
|
114
|
+
stage: "probe" | "output-check" | "remux" | "runner";
|
|
115
|
+
diagnostic: string;
|
|
116
|
+
}>);
|
|
117
|
+
|
|
118
|
+
export type MediaDerivativeReport = Readonly<{
|
|
119
|
+
probe: MediaProbeResult;
|
|
120
|
+
video: MediaDerivativeResult;
|
|
121
|
+
audio: MediaDerivativeResult;
|
|
122
|
+
}>;
|
|
123
|
+
|
|
124
|
+
export type NormalizedPcmWaveHeaderResult =
|
|
125
|
+
| Readonly<{
|
|
126
|
+
ok: true;
|
|
127
|
+
dataBytes: number;
|
|
128
|
+
}>
|
|
129
|
+
| Readonly<{
|
|
130
|
+
ok: false;
|
|
131
|
+
code: "invalid-input" | "invalid-container" | "invalid-format" | "invalid-length" | "empty-audio";
|
|
132
|
+
diagnostic: string;
|
|
133
|
+
}>;
|
|
134
|
+
|
|
135
|
+
export type PcmNormalizationResult =
|
|
136
|
+
| Readonly<{
|
|
137
|
+
status: "created";
|
|
138
|
+
path: string;
|
|
139
|
+
profile: typeof PCM_NORMALIZATION_PROFILE;
|
|
140
|
+
bytes: number;
|
|
141
|
+
}>
|
|
142
|
+
| Readonly<{
|
|
143
|
+
status: "failed";
|
|
144
|
+
path: string;
|
|
145
|
+
profile: typeof PCM_NORMALIZATION_PROFILE;
|
|
146
|
+
stage: "preflight" | "runner" | "process" | "output-check";
|
|
147
|
+
diagnostic: string;
|
|
148
|
+
}>;
|
|
149
|
+
|
|
150
|
+
export type InspectMediaOptions = Readonly<{
|
|
151
|
+
capturePath: string;
|
|
152
|
+
ffprobeExecutable?: string;
|
|
153
|
+
timeoutMs?: number;
|
|
154
|
+
signal?: AbortSignal;
|
|
155
|
+
}>;
|
|
156
|
+
|
|
157
|
+
export type CreateMediaDerivativesOptions = Readonly<{
|
|
158
|
+
capturePath: string;
|
|
159
|
+
derivativesDirectory: string;
|
|
160
|
+
roles: readonly MediaDerivativeRole[];
|
|
161
|
+
ffprobeExecutable?: string;
|
|
162
|
+
ffmpegExecutable?: string;
|
|
163
|
+
probeTimeoutMs?: number;
|
|
164
|
+
remuxTimeoutMs?: number;
|
|
165
|
+
signal?: AbortSignal;
|
|
166
|
+
}>;
|
|
167
|
+
|
|
168
|
+
export type NormalizeAudioForTranscriptionOptions = Readonly<{
|
|
169
|
+
inputPath: string;
|
|
170
|
+
outputPath: string;
|
|
171
|
+
ffmpegExecutable?: string;
|
|
172
|
+
timeoutMs?: number;
|
|
173
|
+
signal?: AbortSignal;
|
|
174
|
+
}>;
|
|
175
|
+
|
|
176
|
+
export type MediaProcessDependencies = Readonly<{
|
|
177
|
+
runProcess: (argv: CommandArgv, options: RunProcessOptions) => Promise<ProcessResult>;
|
|
178
|
+
}>;
|
|
179
|
+
|
|
180
|
+
export type MediaDerivativeDependencies = MediaProcessDependencies;
|
|
181
|
+
|
|
182
|
+
export type PcmNormalizationDependencies = MediaProcessDependencies & Readonly<{
|
|
183
|
+
/** Test seam after a failed output is identified but before identity-checked cleanup. */
|
|
184
|
+
beforeCleanup?: (path: string) => Promise<void>;
|
|
185
|
+
}>;
|
|
186
|
+
|
|
187
|
+
const defaultProcessDependencies: MediaProcessDependencies = {
|
|
188
|
+
runProcess: (argv, options) => runProcess(argv, options),
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const defaultDerivativeDependencies: MediaDerivativeDependencies = defaultProcessDependencies;
|
|
192
|
+
|
|
193
|
+
/** Parses the bounded JSON envelope emitted by Ghostget media's ffprobe invocation. */
|
|
194
|
+
export function parseFfprobeJson(input: unknown): FfprobeParseResult {
|
|
195
|
+
let value: unknown = input;
|
|
196
|
+
if (typeof input === "string") {
|
|
197
|
+
if (input.length > MAX_FFPROBE_JSON_CODE_UNITS) {
|
|
198
|
+
return parseFailure(
|
|
199
|
+
"invalid-json",
|
|
200
|
+
`ffprobe JSON exceeds the ${String(MAX_FFPROBE_JSON_CODE_UNITS)} code-unit limit.`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
value = JSON.parse(input) as unknown;
|
|
205
|
+
} catch {
|
|
206
|
+
return parseFailure("invalid-json", "ffprobe did not return valid JSON.");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!isRecord(value)) {
|
|
211
|
+
return parseFailure("invalid-root", "ffprobe JSON must be an object.");
|
|
212
|
+
}
|
|
213
|
+
const rawStreams = value.streams;
|
|
214
|
+
if (!Array.isArray(rawStreams)) {
|
|
215
|
+
return parseFailure("invalid-streams", "ffprobe JSON must contain a streams array.");
|
|
216
|
+
}
|
|
217
|
+
if (rawStreams.length > MAX_INSPECTED_STREAMS) {
|
|
218
|
+
return parseFailure(
|
|
219
|
+
"too-many-streams",
|
|
220
|
+
`ffprobe reported more than ${String(MAX_INSPECTED_STREAMS)} streams.`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const streams: InspectedMediaStream[] = [];
|
|
225
|
+
for (let position = 0; position < rawStreams.length; position += 1) {
|
|
226
|
+
const rawStream: unknown = rawStreams[position];
|
|
227
|
+
if (!isRecord(rawStream)) {
|
|
228
|
+
return invalidStream(position, "must be an object");
|
|
229
|
+
}
|
|
230
|
+
const index = rawStream.index;
|
|
231
|
+
if (!isNonnegativeSafeInteger(index)) {
|
|
232
|
+
return invalidStream(position, "has an invalid index");
|
|
233
|
+
}
|
|
234
|
+
const codecType = rawStream.codec_type;
|
|
235
|
+
if (typeof codecType !== "string" || codecType.length === 0) {
|
|
236
|
+
return invalidStream(position, "has an invalid codec_type");
|
|
237
|
+
}
|
|
238
|
+
const codecName = rawStream.codec_name;
|
|
239
|
+
if (codecName !== undefined && codecName !== null && typeof codecName !== "string") {
|
|
240
|
+
return invalidStream(position, "has an invalid codec_name");
|
|
241
|
+
}
|
|
242
|
+
streams.push({
|
|
243
|
+
index,
|
|
244
|
+
kind: streamKind(codecType),
|
|
245
|
+
codecName: typeof codecName === "string" && codecName.length > 0 ? codecName : null,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const firstVideo = streams.find((stream) => stream.kind === "video");
|
|
250
|
+
const firstAudio = streams.find((stream) => stream.kind === "audio");
|
|
251
|
+
return {
|
|
252
|
+
ok: true,
|
|
253
|
+
inspection: {
|
|
254
|
+
streams,
|
|
255
|
+
hasVideo: firstVideo !== undefined,
|
|
256
|
+
hasAudio: firstAudio !== undefined,
|
|
257
|
+
firstVideoStreamIndex: firstVideo?.index ?? null,
|
|
258
|
+
firstAudioStreamIndex: firstAudio?.index ?? null,
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Builds the complete shell-free ffprobe argv used to inspect a capture. */
|
|
264
|
+
export function buildFfprobeArgv(
|
|
265
|
+
capturePath: string,
|
|
266
|
+
ffprobeExecutable = "ffprobe",
|
|
267
|
+
): CommandArgv {
|
|
268
|
+
return [
|
|
269
|
+
ffprobeExecutable,
|
|
270
|
+
"-v",
|
|
271
|
+
"error",
|
|
272
|
+
"-show_entries",
|
|
273
|
+
"stream=index,codec_type,codec_name",
|
|
274
|
+
"-of",
|
|
275
|
+
"json",
|
|
276
|
+
capturePath,
|
|
277
|
+
];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Builds a stream-copy command for one primary stream. `-n` is deliberately
|
|
282
|
+
* retained even after a preflight check so a concurrent writer cannot be
|
|
283
|
+
* overwritten between the check and FFmpeg opening the output.
|
|
284
|
+
*/
|
|
285
|
+
export function buildFfmpegDerivativeArgv(
|
|
286
|
+
role: MediaDerivativeRole,
|
|
287
|
+
capturePath: string,
|
|
288
|
+
outputPath: string,
|
|
289
|
+
ffmpegExecutable = "ffmpeg",
|
|
290
|
+
): CommandArgv {
|
|
291
|
+
return [
|
|
292
|
+
ffmpegExecutable,
|
|
293
|
+
"-nostdin",
|
|
294
|
+
"-hide_banner",
|
|
295
|
+
"-loglevel",
|
|
296
|
+
"error",
|
|
297
|
+
"-n",
|
|
298
|
+
"-i",
|
|
299
|
+
capturePath,
|
|
300
|
+
"-map",
|
|
301
|
+
role === "video" ? "0:v:0" : "0:a:0",
|
|
302
|
+
"-map_metadata",
|
|
303
|
+
"0",
|
|
304
|
+
"-map_chapters",
|
|
305
|
+
"0",
|
|
306
|
+
"-c",
|
|
307
|
+
"copy",
|
|
308
|
+
"-f",
|
|
309
|
+
"matroska",
|
|
310
|
+
outputPath,
|
|
311
|
+
];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Builds the complete, shell-free command for Ghostget media's transcription PCM profile. */
|
|
315
|
+
export function buildPcmNormalizationArgv(
|
|
316
|
+
inputPath: string,
|
|
317
|
+
outputPath: string,
|
|
318
|
+
ffmpegExecutable = "ffmpeg",
|
|
319
|
+
): CommandArgv {
|
|
320
|
+
return [
|
|
321
|
+
ffmpegExecutable,
|
|
322
|
+
"-nostdin",
|
|
323
|
+
"-hide_banner",
|
|
324
|
+
"-loglevel",
|
|
325
|
+
"error",
|
|
326
|
+
"-n",
|
|
327
|
+
"-i",
|
|
328
|
+
inputPath,
|
|
329
|
+
"-map",
|
|
330
|
+
"0:a:0",
|
|
331
|
+
"-vn",
|
|
332
|
+
"-sn",
|
|
333
|
+
"-dn",
|
|
334
|
+
"-map_metadata",
|
|
335
|
+
"-1",
|
|
336
|
+
"-map_chapters",
|
|
337
|
+
"-1",
|
|
338
|
+
"-ac",
|
|
339
|
+
String(PCM_CHANNELS),
|
|
340
|
+
"-ar",
|
|
341
|
+
String(PCM_SAMPLE_RATE),
|
|
342
|
+
"-c:a",
|
|
343
|
+
"pcm_s16le",
|
|
344
|
+
"-flags:a",
|
|
345
|
+
"+bitexact",
|
|
346
|
+
"-fflags",
|
|
347
|
+
"+bitexact",
|
|
348
|
+
"-f",
|
|
349
|
+
"wav",
|
|
350
|
+
"-fs",
|
|
351
|
+
String(PCM_NORMALIZATION_MAX_BYTES),
|
|
352
|
+
outputPath,
|
|
353
|
+
];
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Parses only the canonical seekable RIFF/WAVE envelope emitted by Ghostget media's
|
|
358
|
+
* fixed PCM command. Extra chunks and streaming/RF64 variants fail closed.
|
|
359
|
+
*/
|
|
360
|
+
export function parseNormalizedPcmWaveHeader(
|
|
361
|
+
input: unknown,
|
|
362
|
+
fileBytes: unknown,
|
|
363
|
+
): NormalizedPcmWaveHeaderResult {
|
|
364
|
+
if (!(input instanceof Uint8Array) || !Number.isSafeInteger(fileBytes)) {
|
|
365
|
+
return pcmHeaderFailure("invalid-input", "Normalized PCM validation requires a bounded byte header and file length.");
|
|
366
|
+
}
|
|
367
|
+
const boundedFileBytes = fileBytes as number;
|
|
368
|
+
if (
|
|
369
|
+
input.byteLength < PCM_WAVE_HEADER_BYTES
|
|
370
|
+
|| boundedFileBytes < PCM_WAVE_HEADER_BYTES
|
|
371
|
+
|| boundedFileBytes > PCM_NORMALIZATION_MAX_BYTES
|
|
372
|
+
) {
|
|
373
|
+
return pcmHeaderFailure("invalid-length", "Normalized PCM WAV has an invalid bounded length.");
|
|
374
|
+
}
|
|
375
|
+
if (
|
|
376
|
+
!asciiEquals(input, 0, "RIFF")
|
|
377
|
+
|| !asciiEquals(input, 8, "WAVE")
|
|
378
|
+
|| !asciiEquals(input, 12, "fmt ")
|
|
379
|
+
|| !asciiEquals(input, 36, "data")
|
|
380
|
+
) {
|
|
381
|
+
return pcmHeaderFailure("invalid-container", "Normalized PCM output is not Ghostget media's canonical RIFF/WAVE envelope.");
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const view = new DataView(input.buffer, input.byteOffset, input.byteLength);
|
|
385
|
+
const riffBytes = view.getUint32(4, true);
|
|
386
|
+
const formatChunkBytes = view.getUint32(16, true);
|
|
387
|
+
const audioFormat = view.getUint16(20, true);
|
|
388
|
+
const channels = view.getUint16(22, true);
|
|
389
|
+
const sampleRate = view.getUint32(24, true);
|
|
390
|
+
const byteRate = view.getUint32(28, true);
|
|
391
|
+
const blockAlign = view.getUint16(32, true);
|
|
392
|
+
const bitsPerSample = view.getUint16(34, true);
|
|
393
|
+
const dataBytes = view.getUint32(40, true);
|
|
394
|
+
if (
|
|
395
|
+
formatChunkBytes !== 16
|
|
396
|
+
|| audioFormat !== 1
|
|
397
|
+
|| channels !== PCM_CHANNELS
|
|
398
|
+
|| sampleRate !== PCM_SAMPLE_RATE
|
|
399
|
+
|| byteRate !== PCM_BYTES_PER_SECOND
|
|
400
|
+
|| blockAlign !== PCM_BLOCK_ALIGN
|
|
401
|
+
|| bitsPerSample !== PCM_BITS_PER_SAMPLE
|
|
402
|
+
) {
|
|
403
|
+
return pcmHeaderFailure("invalid-format", "Normalized PCM WAV does not match signed 16-bit little-endian mono at 16 kHz.");
|
|
404
|
+
}
|
|
405
|
+
if (dataBytes === 0) {
|
|
406
|
+
return pcmHeaderFailure("empty-audio", "Normalized PCM WAV contains no audio samples.");
|
|
407
|
+
}
|
|
408
|
+
if (
|
|
409
|
+
dataBytes % PCM_BLOCK_ALIGN !== 0
|
|
410
|
+
|| riffBytes !== boundedFileBytes - 8
|
|
411
|
+
|| dataBytes !== boundedFileBytes - PCM_WAVE_HEADER_BYTES
|
|
412
|
+
) {
|
|
413
|
+
return pcmHeaderFailure("invalid-length", "Normalized PCM WAV chunk lengths do not match the physical file.");
|
|
414
|
+
}
|
|
415
|
+
return { ok: true, dataBytes };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Produces one bounded, canonical PCM file for a local transcriber. Existing
|
|
420
|
+
* output paths are never accepted or replaced, and failed output is removed
|
|
421
|
+
* only while it still has the exact identity observed after this invocation.
|
|
422
|
+
*/
|
|
423
|
+
export async function normalizeAudioForTranscription(
|
|
424
|
+
options: NormalizeAudioForTranscriptionOptions,
|
|
425
|
+
dependencies: PcmNormalizationDependencies = defaultProcessDependencies,
|
|
426
|
+
): Promise<PcmNormalizationResult> {
|
|
427
|
+
const timeoutMs = options.timeoutMs ?? PCM_NORMALIZATION_TIMEOUT_MS;
|
|
428
|
+
if (
|
|
429
|
+
!Number.isSafeInteger(timeoutMs)
|
|
430
|
+
|| timeoutMs <= 0
|
|
431
|
+
|| timeoutMs > PCM_NORMALIZATION_TIMEOUT_MS
|
|
432
|
+
) {
|
|
433
|
+
return pcmNormalizationFailure(
|
|
434
|
+
options.outputPath,
|
|
435
|
+
"preflight",
|
|
436
|
+
`PCM normalization timeout must be between 1 and ${String(PCM_NORMALIZATION_TIMEOUT_MS)} milliseconds.`,
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
let before: PcmPathEntry;
|
|
441
|
+
try {
|
|
442
|
+
before = await inspectPcmPath(options.outputPath);
|
|
443
|
+
} catch {
|
|
444
|
+
return pcmNormalizationFailure(
|
|
445
|
+
options.outputPath,
|
|
446
|
+
"preflight",
|
|
447
|
+
"Could not safely inspect the normalized PCM output path.",
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
if (before.kind !== "missing") {
|
|
451
|
+
return pcmNormalizationFailure(
|
|
452
|
+
options.outputPath,
|
|
453
|
+
"preflight",
|
|
454
|
+
before.kind === "regular"
|
|
455
|
+
? "The normalized PCM output path already exists; refusing to replace it."
|
|
456
|
+
: `The normalized PCM output path is a foreign ${before.entryType}; refusing to replace it.`,
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const argv = buildPcmNormalizationArgv(
|
|
461
|
+
options.inputPath,
|
|
462
|
+
options.outputPath,
|
|
463
|
+
options.ffmpegExecutable,
|
|
464
|
+
);
|
|
465
|
+
let processResult: ProcessResult;
|
|
466
|
+
try {
|
|
467
|
+
processResult = await dependencies.runProcess(argv, {
|
|
468
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
469
|
+
timeoutMs,
|
|
470
|
+
maxStdoutBytes: REMUX_STDOUT_LIMIT_BYTES,
|
|
471
|
+
maxStderrBytes: TOOL_STDERR_LIMIT_BYTES,
|
|
472
|
+
});
|
|
473
|
+
} catch {
|
|
474
|
+
const cleanup = await discardCreatedPcmOutput(options.outputPath, dependencies);
|
|
475
|
+
return cleanup.ok
|
|
476
|
+
? pcmNormalizationFailure(
|
|
477
|
+
options.outputPath,
|
|
478
|
+
"runner",
|
|
479
|
+
"The PCM normalization process runner failed unexpectedly.",
|
|
480
|
+
)
|
|
481
|
+
: pcmNormalizationFailure(options.outputPath, "output-check", cleanup.diagnostic);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (!processResult.ok) {
|
|
485
|
+
const cleanup = await discardCreatedPcmOutput(options.outputPath, dependencies);
|
|
486
|
+
return cleanup.ok
|
|
487
|
+
? pcmNormalizationFailure(options.outputPath, "process", processResult.diagnostic)
|
|
488
|
+
: pcmNormalizationFailure(options.outputPath, "output-check", cleanup.diagnostic);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const validation = await validatePcmOutput(options.outputPath);
|
|
492
|
+
if (validation.kind === "valid") {
|
|
493
|
+
return {
|
|
494
|
+
status: "created",
|
|
495
|
+
path: options.outputPath,
|
|
496
|
+
profile: PCM_NORMALIZATION_PROFILE,
|
|
497
|
+
bytes: Number(validation.file.bytes),
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
if (validation.kind === "invalid") {
|
|
501
|
+
const cleanup = await removeKnownPcmFile(
|
|
502
|
+
options.outputPath,
|
|
503
|
+
validation.file,
|
|
504
|
+
dependencies,
|
|
505
|
+
);
|
|
506
|
+
if (!cleanup.ok) {
|
|
507
|
+
return pcmNormalizationFailure(options.outputPath, "output-check", cleanup.diagnostic);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return pcmNormalizationFailure(
|
|
511
|
+
options.outputPath,
|
|
512
|
+
"output-check",
|
|
513
|
+
validation.kind === "missing"
|
|
514
|
+
? "FFmpeg did not create the normalized PCM output."
|
|
515
|
+
: validation.diagnostic,
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** Runs ffprobe directly and validates its stdout before exposing it. */
|
|
520
|
+
export async function inspectMedia(
|
|
521
|
+
options: InspectMediaOptions,
|
|
522
|
+
dependencies: MediaProcessDependencies = defaultProcessDependencies,
|
|
523
|
+
): Promise<MediaProbeResult> {
|
|
524
|
+
const argv = buildFfprobeArgv(options.capturePath, options.ffprobeExecutable);
|
|
525
|
+
let processResult: ProcessResult;
|
|
526
|
+
try {
|
|
527
|
+
processResult = await dependencies.runProcess(argv, {
|
|
528
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
529
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS,
|
|
530
|
+
maxStdoutBytes: PROBE_STDOUT_LIMIT_BYTES,
|
|
531
|
+
maxStderrBytes: TOOL_STDERR_LIMIT_BYTES,
|
|
532
|
+
});
|
|
533
|
+
} catch {
|
|
534
|
+
return {
|
|
535
|
+
ok: false,
|
|
536
|
+
reason: "runner",
|
|
537
|
+
diagnostic: "The ffprobe process runner failed unexpectedly.",
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (!processResult.ok) {
|
|
542
|
+
return {
|
|
543
|
+
ok: false,
|
|
544
|
+
reason: "process",
|
|
545
|
+
diagnostic: processResult.diagnostic,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
if (processResult.stdoutTruncated) {
|
|
549
|
+
return {
|
|
550
|
+
ok: false,
|
|
551
|
+
reason: "invalid-output",
|
|
552
|
+
diagnostic: "ffprobe output exceeded Ghostget media's inspection limit.",
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const parsed = parseFfprobeJson(processResult.stdout);
|
|
557
|
+
return parsed.ok
|
|
558
|
+
? { ok: true, inspection: parsed.inspection }
|
|
559
|
+
: {
|
|
560
|
+
ok: false,
|
|
561
|
+
reason: "invalid-output",
|
|
562
|
+
diagnostic: parsed.error.message,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Creates the requested independent, lossless primary-stream derivatives.
|
|
568
|
+
* The capture is read-only and each remux settles independently, so one failed
|
|
569
|
+
* output never discards the capture or prevents the other output from finishing.
|
|
570
|
+
*/
|
|
571
|
+
export async function createMediaDerivatives(
|
|
572
|
+
options: CreateMediaDerivativesOptions,
|
|
573
|
+
dependencies: MediaDerivativeDependencies = defaultDerivativeDependencies,
|
|
574
|
+
): Promise<MediaDerivativeReport> {
|
|
575
|
+
const videoPath = join(options.derivativesDirectory, "video.mkv");
|
|
576
|
+
const audioPath = join(options.derivativesDirectory, "audio.mka");
|
|
577
|
+
const requestedRoles = new Set(options.roles);
|
|
578
|
+
const probe = await inspectMedia(
|
|
579
|
+
{
|
|
580
|
+
capturePath: options.capturePath,
|
|
581
|
+
...(options.ffprobeExecutable === undefined
|
|
582
|
+
? {}
|
|
583
|
+
: { ffprobeExecutable: options.ffprobeExecutable }),
|
|
584
|
+
...(options.probeTimeoutMs === undefined ? {} : { timeoutMs: options.probeTimeoutMs }),
|
|
585
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
586
|
+
},
|
|
587
|
+
dependencies,
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
if (!probe.ok) {
|
|
591
|
+
return {
|
|
592
|
+
probe,
|
|
593
|
+
video: requestedRoles.has("video")
|
|
594
|
+
? probeDerivativeFailure("video", videoPath, probe.diagnostic)
|
|
595
|
+
: { role: "video", path: videoPath, status: "not-requested" },
|
|
596
|
+
audio: requestedRoles.has("audio")
|
|
597
|
+
? probeDerivativeFailure("audio", audioPath, probe.diagnostic)
|
|
598
|
+
: { role: "audio", path: audioPath, status: "not-requested" },
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const [video, audio] = await Promise.all([
|
|
603
|
+
requestedRoles.has("video") ? settleDerivative(
|
|
604
|
+
"video",
|
|
605
|
+
probe.inspection.firstVideoStreamIndex,
|
|
606
|
+
options.capturePath,
|
|
607
|
+
videoPath,
|
|
608
|
+
options.ffprobeExecutable ?? "ffprobe",
|
|
609
|
+
options.ffmpegExecutable ?? "ffmpeg",
|
|
610
|
+
options.probeTimeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS,
|
|
611
|
+
options.remuxTimeoutMs ?? DEFAULT_REMUX_TIMEOUT_MS,
|
|
612
|
+
options.signal,
|
|
613
|
+
dependencies,
|
|
614
|
+
) : Promise.resolve({ role: "video", path: videoPath, status: "not-requested" } as const),
|
|
615
|
+
requestedRoles.has("audio") ? settleDerivative(
|
|
616
|
+
"audio",
|
|
617
|
+
probe.inspection.firstAudioStreamIndex,
|
|
618
|
+
options.capturePath,
|
|
619
|
+
audioPath,
|
|
620
|
+
options.ffprobeExecutable ?? "ffprobe",
|
|
621
|
+
options.ffmpegExecutable ?? "ffmpeg",
|
|
622
|
+
options.probeTimeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS,
|
|
623
|
+
options.remuxTimeoutMs ?? DEFAULT_REMUX_TIMEOUT_MS,
|
|
624
|
+
options.signal,
|
|
625
|
+
dependencies,
|
|
626
|
+
) : Promise.resolve({ role: "audio", path: audioPath, status: "not-requested" } as const),
|
|
627
|
+
]);
|
|
628
|
+
return { probe, video, audio };
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
async function settleDerivative(
|
|
632
|
+
role: MediaDerivativeRole,
|
|
633
|
+
sourceStreamIndex: number | null,
|
|
634
|
+
capturePath: string,
|
|
635
|
+
outputPath: string,
|
|
636
|
+
ffprobeExecutable: string,
|
|
637
|
+
ffmpegExecutable: string,
|
|
638
|
+
probeTimeoutMs: number,
|
|
639
|
+
timeoutMs: number,
|
|
640
|
+
signal: AbortSignal | undefined,
|
|
641
|
+
dependencies: MediaDerivativeDependencies,
|
|
642
|
+
): Promise<MediaDerivativeResult> {
|
|
643
|
+
const partPath = derivativePartPath(outputPath);
|
|
644
|
+
const finalValidation = await validateDerivativePath(
|
|
645
|
+
role,
|
|
646
|
+
outputPath,
|
|
647
|
+
ffprobeExecutable,
|
|
648
|
+
probeTimeoutMs,
|
|
649
|
+
signal,
|
|
650
|
+
dependencies,
|
|
651
|
+
);
|
|
652
|
+
if (finalValidation.kind === "invalid" || finalValidation.kind === "unsafe") {
|
|
653
|
+
return derivativeFailure(role, outputPath, "output-check", finalValidation.diagnostic);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const partValidation = await validateDerivativePath(
|
|
657
|
+
role,
|
|
658
|
+
partPath,
|
|
659
|
+
ffprobeExecutable,
|
|
660
|
+
probeTimeoutMs,
|
|
661
|
+
signal,
|
|
662
|
+
dependencies,
|
|
663
|
+
);
|
|
664
|
+
if (partValidation.kind === "unsafe") {
|
|
665
|
+
return derivativeFailure(role, outputPath, "output-check", partValidation.diagnostic);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (finalValidation.kind === "valid") {
|
|
669
|
+
if (partValidation.kind !== "missing") {
|
|
670
|
+
const cleanup = await removeKnownRegularFile(partPath, partValidation.file);
|
|
671
|
+
if (!cleanup.ok) {
|
|
672
|
+
return derivativeFailure(role, outputPath, "output-check", cleanup.diagnostic);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
if (sourceStreamIndex === null) {
|
|
676
|
+
return derivativeFailure(
|
|
677
|
+
role,
|
|
678
|
+
outputPath,
|
|
679
|
+
"output-check",
|
|
680
|
+
`A ${role} derivative exists even though the capture has no ${role} stream.`,
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
return { role, path: outputPath, status: "exists", sourceStreamIndex };
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (sourceStreamIndex === null) {
|
|
687
|
+
if (partValidation.kind !== "missing") {
|
|
688
|
+
const cleanup = await removeKnownRegularFile(partPath, partValidation.file);
|
|
689
|
+
if (!cleanup.ok) {
|
|
690
|
+
return derivativeFailure(role, outputPath, "output-check", cleanup.diagnostic);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return { role, path: outputPath, status: "not-present" };
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (partValidation.kind === "valid") {
|
|
697
|
+
return await publishValidatedDerivative(
|
|
698
|
+
role,
|
|
699
|
+
sourceStreamIndex,
|
|
700
|
+
outputPath,
|
|
701
|
+
partPath,
|
|
702
|
+
partValidation.file,
|
|
703
|
+
ffprobeExecutable,
|
|
704
|
+
probeTimeoutMs,
|
|
705
|
+
signal,
|
|
706
|
+
dependencies,
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (partValidation.kind === "invalid") {
|
|
711
|
+
const cleanup = await removeKnownRegularFile(partPath, partValidation.file);
|
|
712
|
+
if (!cleanup.ok) {
|
|
713
|
+
return derivativeFailure(role, outputPath, "output-check", cleanup.diagnostic);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const argv = buildFfmpegDerivativeArgv(
|
|
718
|
+
role,
|
|
719
|
+
capturePath,
|
|
720
|
+
partPath,
|
|
721
|
+
ffmpegExecutable,
|
|
722
|
+
);
|
|
723
|
+
let processResult: ProcessResult;
|
|
724
|
+
try {
|
|
725
|
+
processResult = await dependencies.runProcess(argv, {
|
|
726
|
+
...(signal === undefined ? {} : { signal }),
|
|
727
|
+
timeoutMs,
|
|
728
|
+
maxStdoutBytes: REMUX_STDOUT_LIMIT_BYTES,
|
|
729
|
+
maxStderrBytes: TOOL_STDERR_LIMIT_BYTES,
|
|
730
|
+
});
|
|
731
|
+
} catch {
|
|
732
|
+
const cleanup = await discardFailedPartial(partPath);
|
|
733
|
+
return cleanup.ok
|
|
734
|
+
? derivativeFailure(
|
|
735
|
+
role,
|
|
736
|
+
outputPath,
|
|
737
|
+
"runner",
|
|
738
|
+
`The ${role} FFmpeg process runner failed unexpectedly.`,
|
|
739
|
+
)
|
|
740
|
+
: derivativeFailure(role, outputPath, "output-check", cleanup.diagnostic);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (!processResult.ok) {
|
|
744
|
+
const cleanup = await discardFailedPartial(partPath);
|
|
745
|
+
return cleanup.ok
|
|
746
|
+
? derivativeFailure(role, outputPath, "remux", processResult.diagnostic)
|
|
747
|
+
: derivativeFailure(role, outputPath, "output-check", cleanup.diagnostic);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const generatedValidation = await validateDerivativePath(
|
|
751
|
+
role,
|
|
752
|
+
partPath,
|
|
753
|
+
ffprobeExecutable,
|
|
754
|
+
probeTimeoutMs,
|
|
755
|
+
signal,
|
|
756
|
+
dependencies,
|
|
757
|
+
);
|
|
758
|
+
if (generatedValidation.kind !== "valid") {
|
|
759
|
+
if (generatedValidation.kind === "invalid") {
|
|
760
|
+
const cleanup = await removeKnownRegularFile(partPath, generatedValidation.file);
|
|
761
|
+
if (!cleanup.ok) {
|
|
762
|
+
return derivativeFailure(role, outputPath, "output-check", cleanup.diagnostic);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
const diagnostic =
|
|
766
|
+
generatedValidation.kind === "missing"
|
|
767
|
+
? `FFmpeg did not create the ${role} derivative partial.`
|
|
768
|
+
: generatedValidation.diagnostic;
|
|
769
|
+
return derivativeFailure(role, outputPath, "output-check", diagnostic);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return await publishValidatedDerivative(
|
|
773
|
+
role,
|
|
774
|
+
sourceStreamIndex,
|
|
775
|
+
outputPath,
|
|
776
|
+
partPath,
|
|
777
|
+
generatedValidation.file,
|
|
778
|
+
ffprobeExecutable,
|
|
779
|
+
probeTimeoutMs,
|
|
780
|
+
signal,
|
|
781
|
+
dependencies,
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
type RegularDerivativeFile = Readonly<{
|
|
786
|
+
kind: "regular";
|
|
787
|
+
bytes: bigint;
|
|
788
|
+
device: bigint;
|
|
789
|
+
inode: bigint;
|
|
790
|
+
}>;
|
|
791
|
+
|
|
792
|
+
type DerivativePathEntry =
|
|
793
|
+
| Readonly<{ kind: "missing" }>
|
|
794
|
+
| RegularDerivativeFile
|
|
795
|
+
| Readonly<{
|
|
796
|
+
kind: "unsafe";
|
|
797
|
+
entryType: "symbolic link" | "directory" | "non-regular entry";
|
|
798
|
+
}>;
|
|
799
|
+
|
|
800
|
+
type DerivativeValidation =
|
|
801
|
+
| Readonly<{ kind: "missing" }>
|
|
802
|
+
| Readonly<{ kind: "valid"; file: RegularDerivativeFile }>
|
|
803
|
+
| Readonly<{ kind: "invalid"; file: RegularDerivativeFile; diagnostic: string }>
|
|
804
|
+
| Readonly<{ kind: "unsafe"; diagnostic: string }>;
|
|
805
|
+
|
|
806
|
+
type FileMutationResult =
|
|
807
|
+
| Readonly<{ ok: true }>
|
|
808
|
+
| Readonly<{ ok: false; diagnostic: string }>;
|
|
809
|
+
|
|
810
|
+
type RegularPcmFile = Readonly<{
|
|
811
|
+
kind: "regular";
|
|
812
|
+
bytes: bigint;
|
|
813
|
+
device: bigint;
|
|
814
|
+
inode: bigint;
|
|
815
|
+
modifiedAtNanoseconds: bigint;
|
|
816
|
+
changedAtNanoseconds: bigint;
|
|
817
|
+
}>;
|
|
818
|
+
|
|
819
|
+
type PcmPathEntry =
|
|
820
|
+
| Readonly<{ kind: "missing" }>
|
|
821
|
+
| RegularPcmFile
|
|
822
|
+
| Readonly<{
|
|
823
|
+
kind: "unsafe";
|
|
824
|
+
entryType: "symbolic link" | "directory" | "non-regular entry";
|
|
825
|
+
}>;
|
|
826
|
+
|
|
827
|
+
type PcmOutputValidation =
|
|
828
|
+
| Readonly<{ kind: "missing" }>
|
|
829
|
+
| Readonly<{ kind: "valid"; file: RegularPcmFile }>
|
|
830
|
+
| Readonly<{ kind: "invalid"; file: RegularPcmFile; diagnostic: string }>
|
|
831
|
+
| Readonly<{ kind: "unsafe"; diagnostic: string }>;
|
|
832
|
+
|
|
833
|
+
function derivativePartPath(outputPath: string): string {
|
|
834
|
+
return join(dirname(outputPath), `.${basename(outputPath)}.part`);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
async function validateDerivativePath(
|
|
838
|
+
role: MediaDerivativeRole,
|
|
839
|
+
path: string,
|
|
840
|
+
ffprobeExecutable: string,
|
|
841
|
+
timeoutMs: number,
|
|
842
|
+
signal: AbortSignal | undefined,
|
|
843
|
+
dependencies: MediaProcessDependencies,
|
|
844
|
+
): Promise<DerivativeValidation> {
|
|
845
|
+
let before: DerivativePathEntry;
|
|
846
|
+
try {
|
|
847
|
+
before = await inspectDerivativePath(path);
|
|
848
|
+
} catch {
|
|
849
|
+
return {
|
|
850
|
+
kind: "unsafe",
|
|
851
|
+
diagnostic: `Could not safely inspect the ${role} derivative path.`,
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
if (before.kind === "missing") return before;
|
|
855
|
+
if (before.kind === "unsafe") {
|
|
856
|
+
return {
|
|
857
|
+
kind: "unsafe",
|
|
858
|
+
diagnostic: `The ${role} derivative path is a foreign ${before.entryType}; refusing to replace it.`,
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
if (before.bytes === 0n) {
|
|
862
|
+
return {
|
|
863
|
+
kind: "invalid",
|
|
864
|
+
file: before,
|
|
865
|
+
diagnostic: `The ${role} derivative is an empty regular file.`,
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const probe = await inspectMedia(
|
|
870
|
+
{
|
|
871
|
+
capturePath: path,
|
|
872
|
+
ffprobeExecutable,
|
|
873
|
+
timeoutMs,
|
|
874
|
+
...(signal === undefined ? {} : { signal }),
|
|
875
|
+
},
|
|
876
|
+
dependencies,
|
|
877
|
+
);
|
|
878
|
+
if (!probe.ok) {
|
|
879
|
+
return {
|
|
880
|
+
kind: "invalid",
|
|
881
|
+
file: before,
|
|
882
|
+
diagnostic: `The ${role} derivative failed ffprobe validation: ${probe.diagnostic}`,
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
if (!hasExactPrimaryKind(probe.inspection, role)) {
|
|
886
|
+
return {
|
|
887
|
+
kind: "invalid",
|
|
888
|
+
file: before,
|
|
889
|
+
diagnostic: `The ${role} derivative must contain exactly one ${role} stream and no ${oppositeRole(role)} stream.`,
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
let after: DerivativePathEntry;
|
|
894
|
+
try {
|
|
895
|
+
after = await inspectDerivativePath(path);
|
|
896
|
+
} catch {
|
|
897
|
+
return {
|
|
898
|
+
kind: "unsafe",
|
|
899
|
+
diagnostic: `Could not re-inspect the ${role} derivative after ffprobe validation.`,
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
if (after.kind !== "regular" || !sameRegularFile(before, after)) {
|
|
903
|
+
return {
|
|
904
|
+
kind: "unsafe",
|
|
905
|
+
diagnostic: `The ${role} derivative changed during ffprobe validation.`,
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
return { kind: "valid", file: after };
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
async function inspectDerivativePath(path: string): Promise<DerivativePathEntry> {
|
|
912
|
+
try {
|
|
913
|
+
const metadata = await lstat(path, { bigint: true });
|
|
914
|
+
if (metadata.isSymbolicLink()) return { kind: "unsafe", entryType: "symbolic link" };
|
|
915
|
+
if (metadata.isDirectory()) return { kind: "unsafe", entryType: "directory" };
|
|
916
|
+
if (!metadata.isFile()) return { kind: "unsafe", entryType: "non-regular entry" };
|
|
917
|
+
return {
|
|
918
|
+
kind: "regular",
|
|
919
|
+
bytes: metadata.size,
|
|
920
|
+
device: metadata.dev,
|
|
921
|
+
inode: metadata.ino,
|
|
922
|
+
};
|
|
923
|
+
} catch (error) {
|
|
924
|
+
if (isFileNotFoundError(error)) return { kind: "missing" };
|
|
925
|
+
throw error;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
async function validatePcmOutput(path: string): Promise<PcmOutputValidation> {
|
|
930
|
+
let before: PcmPathEntry;
|
|
931
|
+
try {
|
|
932
|
+
before = await inspectPcmPath(path);
|
|
933
|
+
} catch {
|
|
934
|
+
return { kind: "unsafe", diagnostic: "Could not safely inspect the normalized PCM output." };
|
|
935
|
+
}
|
|
936
|
+
if (before.kind === "missing") return before;
|
|
937
|
+
if (before.kind === "unsafe") {
|
|
938
|
+
return {
|
|
939
|
+
kind: "unsafe",
|
|
940
|
+
diagnostic: `The normalized PCM output became a foreign ${before.entryType}; refusing to trust or remove it.`,
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
if (before.bytes <= BigInt(PCM_WAVE_HEADER_BYTES)) {
|
|
944
|
+
return {
|
|
945
|
+
kind: "invalid",
|
|
946
|
+
file: before,
|
|
947
|
+
diagnostic: "The normalized PCM output is empty or lacks audio samples.",
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
if (before.bytes > BigInt(PCM_NORMALIZATION_MAX_BYTES)) {
|
|
951
|
+
return {
|
|
952
|
+
kind: "invalid",
|
|
953
|
+
file: before,
|
|
954
|
+
diagnostic: `The normalized PCM output exceeds the ${String(PCM_NORMALIZATION_MAX_BYTES)} byte limit.`,
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
let handle;
|
|
959
|
+
try {
|
|
960
|
+
handle = await open(
|
|
961
|
+
path,
|
|
962
|
+
constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK,
|
|
963
|
+
);
|
|
964
|
+
} catch {
|
|
965
|
+
return { kind: "unsafe", diagnostic: "Could not safely open the normalized PCM output without following links." };
|
|
966
|
+
}
|
|
967
|
+
try {
|
|
968
|
+
const opened = await handle.stat({ bigint: true });
|
|
969
|
+
const openedFile = pcmFileFromStats(opened);
|
|
970
|
+
if (openedFile === null || !samePcmFile(before, openedFile)) {
|
|
971
|
+
return { kind: "unsafe", diagnostic: "The normalized PCM output changed before validation." };
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
const header = new Uint8Array(PCM_WAVE_HEADER_BYTES);
|
|
975
|
+
let offset = 0;
|
|
976
|
+
while (offset < header.byteLength) {
|
|
977
|
+
const read = await handle.read(
|
|
978
|
+
header,
|
|
979
|
+
offset,
|
|
980
|
+
header.byteLength - offset,
|
|
981
|
+
offset,
|
|
982
|
+
);
|
|
983
|
+
if (read.bytesRead === 0) break;
|
|
984
|
+
offset += read.bytesRead;
|
|
985
|
+
}
|
|
986
|
+
const parsed = parseNormalizedPcmWaveHeader(header.subarray(0, offset), Number(before.bytes));
|
|
987
|
+
|
|
988
|
+
const [finished, after] = await Promise.all([
|
|
989
|
+
handle.stat({ bigint: true }),
|
|
990
|
+
inspectPcmPath(path),
|
|
991
|
+
]);
|
|
992
|
+
const finishedFile = pcmFileFromStats(finished);
|
|
993
|
+
if (
|
|
994
|
+
finishedFile === null
|
|
995
|
+
|| after.kind !== "regular"
|
|
996
|
+
|| !samePcmFile(before, finishedFile)
|
|
997
|
+
|| !samePcmFile(before, after)
|
|
998
|
+
) {
|
|
999
|
+
return { kind: "unsafe", diagnostic: "The normalized PCM output changed during validation." };
|
|
1000
|
+
}
|
|
1001
|
+
return parsed.ok
|
|
1002
|
+
? { kind: "valid", file: after }
|
|
1003
|
+
: { kind: "invalid", file: after, diagnostic: parsed.diagnostic };
|
|
1004
|
+
} catch {
|
|
1005
|
+
return { kind: "unsafe", diagnostic: "Could not safely validate the normalized PCM output." };
|
|
1006
|
+
} finally {
|
|
1007
|
+
try {
|
|
1008
|
+
await handle.close();
|
|
1009
|
+
} catch {
|
|
1010
|
+
// Validation already treats the pathname and opened identity as closed
|
|
1011
|
+
// data. A close failure must not escape the owned result boundary.
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
async function inspectPcmPath(path: string): Promise<PcmPathEntry> {
|
|
1017
|
+
try {
|
|
1018
|
+
const metadata = await lstat(path, { bigint: true });
|
|
1019
|
+
if (metadata.isSymbolicLink()) return { kind: "unsafe", entryType: "symbolic link" };
|
|
1020
|
+
if (metadata.isDirectory()) return { kind: "unsafe", entryType: "directory" };
|
|
1021
|
+
const file = pcmFileFromStats(metadata);
|
|
1022
|
+
return file ?? { kind: "unsafe", entryType: "non-regular entry" };
|
|
1023
|
+
} catch (error) {
|
|
1024
|
+
if (isFileNotFoundError(error)) return { kind: "missing" };
|
|
1025
|
+
throw error;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function pcmFileFromStats(metadata: BigIntStats): RegularPcmFile | null {
|
|
1030
|
+
if (!metadata.isFile()) return null;
|
|
1031
|
+
return {
|
|
1032
|
+
kind: "regular",
|
|
1033
|
+
bytes: metadata.size,
|
|
1034
|
+
device: metadata.dev,
|
|
1035
|
+
inode: metadata.ino,
|
|
1036
|
+
modifiedAtNanoseconds: metadata.mtimeNs,
|
|
1037
|
+
changedAtNanoseconds: metadata.ctimeNs,
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function samePcmFile(left: RegularPcmFile, right: RegularPcmFile): boolean {
|
|
1042
|
+
return left.device === right.device
|
|
1043
|
+
&& left.inode === right.inode
|
|
1044
|
+
&& left.bytes === right.bytes
|
|
1045
|
+
&& left.modifiedAtNanoseconds === right.modifiedAtNanoseconds
|
|
1046
|
+
&& left.changedAtNanoseconds === right.changedAtNanoseconds;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
async function discardCreatedPcmOutput(
|
|
1050
|
+
path: string,
|
|
1051
|
+
dependencies: PcmNormalizationDependencies,
|
|
1052
|
+
): Promise<FileMutationResult> {
|
|
1053
|
+
let entry: PcmPathEntry;
|
|
1054
|
+
try {
|
|
1055
|
+
entry = await inspectPcmPath(path);
|
|
1056
|
+
} catch {
|
|
1057
|
+
return { ok: false, diagnostic: "Could not safely inspect failed normalized PCM output." };
|
|
1058
|
+
}
|
|
1059
|
+
if (entry.kind === "missing") return { ok: true };
|
|
1060
|
+
if (entry.kind === "unsafe") {
|
|
1061
|
+
return {
|
|
1062
|
+
ok: false,
|
|
1063
|
+
diagnostic: `Failed normalized PCM output became a foreign ${entry.entryType}; refusing to remove it.`,
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
return await removeKnownPcmFile(path, entry, dependencies);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
async function removeKnownPcmFile(
|
|
1070
|
+
path: string,
|
|
1071
|
+
expected: RegularPcmFile,
|
|
1072
|
+
dependencies: PcmNormalizationDependencies,
|
|
1073
|
+
): Promise<FileMutationResult> {
|
|
1074
|
+
try {
|
|
1075
|
+
await dependencies.beforeCleanup?.(path);
|
|
1076
|
+
} catch {
|
|
1077
|
+
return { ok: false, diagnostic: "Could not safely prepare normalized PCM cleanup." };
|
|
1078
|
+
}
|
|
1079
|
+
let current: PcmPathEntry;
|
|
1080
|
+
try {
|
|
1081
|
+
current = await inspectPcmPath(path);
|
|
1082
|
+
} catch {
|
|
1083
|
+
return { ok: false, diagnostic: "Could not safely re-inspect normalized PCM output before cleanup." };
|
|
1084
|
+
}
|
|
1085
|
+
if (current.kind === "missing") return { ok: true };
|
|
1086
|
+
if (current.kind !== "regular" || !samePcmFile(current, expected)) {
|
|
1087
|
+
return {
|
|
1088
|
+
ok: false,
|
|
1089
|
+
diagnostic: "Normalized PCM output changed before cleanup; refusing to remove it.",
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
try {
|
|
1093
|
+
await unlink(path);
|
|
1094
|
+
return { ok: true };
|
|
1095
|
+
} catch (error) {
|
|
1096
|
+
if (isFileNotFoundError(error)) return { ok: true };
|
|
1097
|
+
return { ok: false, diagnostic: "Could not safely remove failed normalized PCM output." };
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function hasExactPrimaryKind(
|
|
1102
|
+
inspection: MediaInspection,
|
|
1103
|
+
role: MediaDerivativeRole,
|
|
1104
|
+
): boolean {
|
|
1105
|
+
const opposite = oppositeRole(role);
|
|
1106
|
+
return (
|
|
1107
|
+
inspection.streams.filter((stream) => stream.kind === role).length === 1 &&
|
|
1108
|
+
inspection.streams.every((stream) => stream.kind !== opposite)
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
function oppositeRole(role: MediaDerivativeRole): MediaDerivativeRole {
|
|
1113
|
+
return role === "video" ? "audio" : "video";
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function sameRegularFile(
|
|
1117
|
+
left: RegularDerivativeFile,
|
|
1118
|
+
right: RegularDerivativeFile,
|
|
1119
|
+
): boolean {
|
|
1120
|
+
return (
|
|
1121
|
+
left.device === right.device &&
|
|
1122
|
+
left.inode === right.inode &&
|
|
1123
|
+
left.bytes === right.bytes
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
async function removeKnownRegularFile(
|
|
1128
|
+
path: string,
|
|
1129
|
+
expected: RegularDerivativeFile,
|
|
1130
|
+
): Promise<FileMutationResult> {
|
|
1131
|
+
let current: DerivativePathEntry;
|
|
1132
|
+
try {
|
|
1133
|
+
current = await inspectDerivativePath(path);
|
|
1134
|
+
} catch {
|
|
1135
|
+
return { ok: false, diagnostic: "Could not safely re-inspect a derivative partial." };
|
|
1136
|
+
}
|
|
1137
|
+
if (current.kind === "missing") return { ok: true };
|
|
1138
|
+
if (current.kind !== "regular" || !sameRegularFile(current, expected)) {
|
|
1139
|
+
return {
|
|
1140
|
+
ok: false,
|
|
1141
|
+
diagnostic: "A derivative partial changed before cleanup; refusing to remove it.",
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
try {
|
|
1145
|
+
await unlink(path);
|
|
1146
|
+
return { ok: true };
|
|
1147
|
+
} catch (error) {
|
|
1148
|
+
if (isFileNotFoundError(error)) return { ok: true };
|
|
1149
|
+
return { ok: false, diagnostic: "Could not safely remove a derivative partial." };
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
async function discardFailedPartial(partPath: string): Promise<FileMutationResult> {
|
|
1154
|
+
let entry: DerivativePathEntry;
|
|
1155
|
+
try {
|
|
1156
|
+
entry = await inspectDerivativePath(partPath);
|
|
1157
|
+
} catch {
|
|
1158
|
+
return { ok: false, diagnostic: "Could not safely inspect a failed derivative partial." };
|
|
1159
|
+
}
|
|
1160
|
+
if (entry.kind === "missing") return { ok: true };
|
|
1161
|
+
if (entry.kind === "unsafe") {
|
|
1162
|
+
return {
|
|
1163
|
+
ok: false,
|
|
1164
|
+
diagnostic: `A failed derivative partial became a foreign ${entry.entryType}; refusing to remove it.`,
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
return await removeKnownRegularFile(partPath, entry);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
async function publishValidatedDerivative(
|
|
1171
|
+
role: MediaDerivativeRole,
|
|
1172
|
+
sourceStreamIndex: number,
|
|
1173
|
+
outputPath: string,
|
|
1174
|
+
partPath: string,
|
|
1175
|
+
validatedPart: RegularDerivativeFile,
|
|
1176
|
+
ffprobeExecutable: string,
|
|
1177
|
+
probeTimeoutMs: number,
|
|
1178
|
+
signal: AbortSignal | undefined,
|
|
1179
|
+
dependencies: MediaProcessDependencies,
|
|
1180
|
+
): Promise<MediaDerivativeResult> {
|
|
1181
|
+
try {
|
|
1182
|
+
await link(partPath, outputPath);
|
|
1183
|
+
} catch (error) {
|
|
1184
|
+
if (!hasErrorCode(error, "EEXIST")) {
|
|
1185
|
+
return derivativeFailure(
|
|
1186
|
+
role,
|
|
1187
|
+
outputPath,
|
|
1188
|
+
"output-check",
|
|
1189
|
+
`Could not atomically publish the ${role} derivative without overwrite.`,
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
const concurrent = await validateDerivativePath(
|
|
1193
|
+
role,
|
|
1194
|
+
outputPath,
|
|
1195
|
+
ffprobeExecutable,
|
|
1196
|
+
probeTimeoutMs,
|
|
1197
|
+
signal,
|
|
1198
|
+
dependencies,
|
|
1199
|
+
);
|
|
1200
|
+
if (concurrent.kind !== "valid") {
|
|
1201
|
+
const diagnostic =
|
|
1202
|
+
concurrent.kind === "missing"
|
|
1203
|
+
? `The ${role} derivative disappeared during no-clobber publication.`
|
|
1204
|
+
: concurrent.diagnostic;
|
|
1205
|
+
return derivativeFailure(
|
|
1206
|
+
role,
|
|
1207
|
+
outputPath,
|
|
1208
|
+
"output-check",
|
|
1209
|
+
`Refused to clobber an existing derivative. ${diagnostic}`,
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
const cleanup = await removeKnownRegularFile(partPath, validatedPart);
|
|
1213
|
+
return cleanup.ok
|
|
1214
|
+
? { role, path: outputPath, status: "exists", sourceStreamIndex }
|
|
1215
|
+
: derivativeFailure(role, outputPath, "output-check", cleanup.diagnostic);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
let published: DerivativePathEntry;
|
|
1219
|
+
try {
|
|
1220
|
+
published = await inspectDerivativePath(outputPath);
|
|
1221
|
+
} catch {
|
|
1222
|
+
return derivativeFailure(
|
|
1223
|
+
role,
|
|
1224
|
+
outputPath,
|
|
1225
|
+
"output-check",
|
|
1226
|
+
`Could not verify the atomically published ${role} derivative.`,
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
if (published.kind !== "regular" || !sameRegularFile(published, validatedPart)) {
|
|
1230
|
+
return derivativeFailure(
|
|
1231
|
+
role,
|
|
1232
|
+
outputPath,
|
|
1233
|
+
"output-check",
|
|
1234
|
+
`The atomically published ${role} derivative does not match its validated partial.`,
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
const cleanup = await removeKnownRegularFile(partPath, validatedPart);
|
|
1238
|
+
return cleanup.ok
|
|
1239
|
+
? { role, path: outputPath, status: "created", sourceStreamIndex }
|
|
1240
|
+
: derivativeFailure(role, outputPath, "output-check", cleanup.diagnostic);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
function derivativeFailure(
|
|
1244
|
+
role: MediaDerivativeRole,
|
|
1245
|
+
path: string,
|
|
1246
|
+
stage: "probe" | "output-check" | "remux" | "runner",
|
|
1247
|
+
diagnostic: string,
|
|
1248
|
+
): MediaDerivativeResult {
|
|
1249
|
+
return { role, path, status: "failed", stage, diagnostic };
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function probeDerivativeFailure(
|
|
1253
|
+
role: MediaDerivativeRole,
|
|
1254
|
+
path: string,
|
|
1255
|
+
diagnostic: string,
|
|
1256
|
+
): MediaDerivativeResult {
|
|
1257
|
+
return { role, path, status: "failed", stage: "probe", diagnostic };
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
function pcmNormalizationFailure(
|
|
1261
|
+
path: string,
|
|
1262
|
+
stage: "preflight" | "runner" | "process" | "output-check",
|
|
1263
|
+
diagnostic: string,
|
|
1264
|
+
): PcmNormalizationResult {
|
|
1265
|
+
return {
|
|
1266
|
+
status: "failed",
|
|
1267
|
+
path,
|
|
1268
|
+
profile: PCM_NORMALIZATION_PROFILE,
|
|
1269
|
+
stage,
|
|
1270
|
+
diagnostic,
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function pcmHeaderFailure(
|
|
1275
|
+
code: "invalid-input" | "invalid-container" | "invalid-format" | "invalid-length" | "empty-audio",
|
|
1276
|
+
diagnostic: string,
|
|
1277
|
+
): NormalizedPcmWaveHeaderResult {
|
|
1278
|
+
return { ok: false, code, diagnostic };
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
function asciiEquals(input: Uint8Array, offset: number, expected: string): boolean {
|
|
1282
|
+
if (offset < 0 || offset + expected.length > input.byteLength) return false;
|
|
1283
|
+
for (let index = 0; index < expected.length; index += 1) {
|
|
1284
|
+
if (input[offset + index] !== expected.charCodeAt(index)) return false;
|
|
1285
|
+
}
|
|
1286
|
+
return true;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function parseFailure(
|
|
1290
|
+
code: FfprobeParseErrorCode,
|
|
1291
|
+
message: string,
|
|
1292
|
+
): FfprobeParseResult {
|
|
1293
|
+
return { ok: false, error: { code, message } };
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
function invalidStream(position: number, detail: string): FfprobeParseResult {
|
|
1297
|
+
return parseFailure(
|
|
1298
|
+
"invalid-stream",
|
|
1299
|
+
`ffprobe stream at position ${String(position)} ${detail}.`,
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
function streamKind(codecType: string): MediaStreamKind {
|
|
1304
|
+
switch (codecType) {
|
|
1305
|
+
case "video":
|
|
1306
|
+
case "audio":
|
|
1307
|
+
case "subtitle":
|
|
1308
|
+
case "data":
|
|
1309
|
+
case "attachment":
|
|
1310
|
+
return codecType;
|
|
1311
|
+
default:
|
|
1312
|
+
return "other";
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
1317
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
function isNonnegativeSafeInteger(value: unknown): value is number {
|
|
1321
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function isFileNotFoundError(error: unknown): boolean {
|
|
1325
|
+
return hasErrorCode(error, "ENOENT");
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
function hasErrorCode(error: unknown, code: string): boolean {
|
|
1329
|
+
return isRecord(error) && error.code === code;
|
|
1330
|
+
}
|