fix video embeds (#12)
This commit is contained in:
@@ -10,6 +10,11 @@ Embed Bluesky links in Discord.
|
||||
|
||||
#### Simply append `x` at the end of `bsky.app`.
|
||||
|
||||
## FAQ
|
||||
|
||||
### [Video is too long to embed] in an embed description.
|
||||
Due to Discord's and BlueSky limitations, the video cannot be embedded if it exceeds ~30s. You can still view the video by clicking on the `VixBluesky` in the author field.
|
||||
|
||||
## Authors
|
||||
|
||||
- [@ItsRauf](https://www.github.com/ItsRauf) - Original author
|
||||
|
||||
@@ -2,6 +2,8 @@ FROM node:22-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add ffmpeg
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm i -g pnpm && pnpm install
|
||||
|
||||
@@ -11,12 +11,9 @@
|
||||
"author": "Lexedia",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"fastify": "^4.28.1",
|
||||
"ffmpeg-static": "^5.2.0",
|
||||
"fluent-ffmpeg": "^2.1.3"
|
||||
"fastify": "^4.28.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/fluent-ffmpeg": "^2.1.26",
|
||||
"@types/node": "^22.5.4",
|
||||
"pino-pretty": "^11.2.2",
|
||||
"typescript": "^5.6.2"
|
||||
|
||||
181
pkgs/api/pnpm-lock.yaml
generated
181
pkgs/api/pnpm-lock.yaml
generated
@@ -11,16 +11,7 @@ importers:
|
||||
fastify:
|
||||
specifier: ^4.28.1
|
||||
version: 4.28.1
|
||||
ffmpeg-static:
|
||||
specifier: ^5.2.0
|
||||
version: 5.2.0
|
||||
fluent-ffmpeg:
|
||||
specifier: ^2.1.3
|
||||
version: 2.1.3
|
||||
devDependencies:
|
||||
'@types/fluent-ffmpeg':
|
||||
specifier: ^2.1.26
|
||||
version: 2.1.26
|
||||
'@types/node':
|
||||
specifier: ^22.5.4
|
||||
version: 22.5.4
|
||||
@@ -33,10 +24,6 @@ importers:
|
||||
|
||||
packages:
|
||||
|
||||
'@derhuerst/http-basic@8.2.4':
|
||||
resolution: {integrity: sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@fastify/ajv-compiler@3.6.0':
|
||||
resolution: {integrity: sha512-LwdXQJjmMD+GwLOkP7TVC68qa+pSSogeWWmznRJ/coyTcfe9qA05AHFSe1eZFwK6q+xVRpChnvFUkf1iYaSZsQ==}
|
||||
|
||||
@@ -49,12 +36,6 @@ packages:
|
||||
'@fastify/merge-json-schemas@0.1.1':
|
||||
resolution: {integrity: sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==}
|
||||
|
||||
'@types/fluent-ffmpeg@2.1.26':
|
||||
resolution: {integrity: sha512-0JVF3wdQG+pN0ImwWD0bNgJiKF2OHg/7CDBHw5UIbRTvlnkgGHK6V5doE54ltvhud4o31/dEiHm23CAlxFiUQg==}
|
||||
|
||||
'@types/node@10.17.60':
|
||||
resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==}
|
||||
|
||||
'@types/node@22.5.4':
|
||||
resolution: {integrity: sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==}
|
||||
|
||||
@@ -65,10 +46,6 @@ packages:
|
||||
abstract-logging@2.0.1:
|
||||
resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==}
|
||||
|
||||
agent-base@6.0.2:
|
||||
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
|
||||
engines: {node: '>= 6.0.0'}
|
||||
|
||||
ajv-formats@2.1.1:
|
||||
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
|
||||
peerDependencies:
|
||||
@@ -88,9 +65,6 @@ packages:
|
||||
ajv@8.17.1:
|
||||
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
|
||||
|
||||
async@0.2.10:
|
||||
resolution: {integrity: sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==}
|
||||
|
||||
atomic-sleep@1.0.0:
|
||||
resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
@@ -101,22 +75,12 @@ packages:
|
||||
base64-js@1.5.1:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
|
||||
buffer-from@1.1.2:
|
||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||
|
||||
buffer@6.0.3:
|
||||
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
||||
|
||||
caseless@0.12.0:
|
||||
resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
|
||||
|
||||
colorette@2.0.20:
|
||||
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
|
||||
|
||||
concat-stream@2.0.0:
|
||||
resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==}
|
||||
engines: {'0': node >= 6.0}
|
||||
|
||||
cookie@0.6.0:
|
||||
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -124,22 +88,9 @@ packages:
|
||||
dateformat@4.6.3:
|
||||
resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==}
|
||||
|
||||
debug@4.3.7:
|
||||
resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==}
|
||||
engines: {node: '>=6.0'}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
end-of-stream@1.4.4:
|
||||
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
|
||||
|
||||
env-paths@2.2.1:
|
||||
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
event-target-shim@5.0.1:
|
||||
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -185,18 +136,10 @@ packages:
|
||||
fastq@1.17.1:
|
||||
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
|
||||
|
||||
ffmpeg-static@5.2.0:
|
||||
resolution: {integrity: sha512-WrM7kLW+do9HLr+H6tk7LzQ7kPqbAgLjdzNE32+u3Ff11gXt9Kkkd2nusGFrlWMIe+XaA97t+I8JS7sZIrvRgA==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
find-my-way@8.2.0:
|
||||
resolution: {integrity: sha512-HdWXgFYc6b1BJcOBDBwjqWuHJj1WYiqrxSh25qtU4DabpMFdj/gSunNBQb83t+8Zt67D7CXEzJWTkxaShMTMOA==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
fluent-ffmpeg@2.1.3:
|
||||
resolution: {integrity: sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
forwarded@0.2.0:
|
||||
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -204,26 +147,13 @@ packages:
|
||||
help-me@5.0.0:
|
||||
resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==}
|
||||
|
||||
http-response-object@3.0.2:
|
||||
resolution: {integrity: sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==}
|
||||
|
||||
https-proxy-agent@5.0.1:
|
||||
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
ieee754@1.2.1:
|
||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||
|
||||
inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
|
||||
ipaddr.js@1.9.1:
|
||||
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
isexe@2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
|
||||
joycon@3.1.1:
|
||||
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -240,9 +170,6 @@ packages:
|
||||
minimist@1.2.8:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
on-exit-leak-free@2.1.2:
|
||||
resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@@ -250,9 +177,6 @@ packages:
|
||||
once@1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
|
||||
parse-cache-control@1.0.1:
|
||||
resolution: {integrity: sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==}
|
||||
|
||||
pino-abstract-transport@1.2.0:
|
||||
resolution: {integrity: sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==}
|
||||
|
||||
@@ -277,10 +201,6 @@ packages:
|
||||
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
|
||||
engines: {node: '>= 0.6.0'}
|
||||
|
||||
progress@2.0.3:
|
||||
resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
proxy-addr@2.0.7:
|
||||
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
||||
engines: {node: '>= 0.10'}
|
||||
@@ -291,10 +211,6 @@ packages:
|
||||
quick-format-unescaped@4.0.4:
|
||||
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
|
||||
|
||||
readable-stream@3.6.2:
|
||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
readable-stream@4.5.2:
|
||||
resolution: {integrity: sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
@@ -360,9 +276,6 @@ packages:
|
||||
resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
typedarray@0.0.6:
|
||||
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
|
||||
|
||||
typescript@5.6.2:
|
||||
resolution: {integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==}
|
||||
engines: {node: '>=14.17'}
|
||||
@@ -371,25 +284,11 @@ packages:
|
||||
undici-types@6.19.8:
|
||||
resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
|
||||
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
which@1.3.1:
|
||||
resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
|
||||
hasBin: true
|
||||
|
||||
wrappy@1.0.2:
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@derhuerst/http-basic@8.2.4':
|
||||
dependencies:
|
||||
caseless: 0.12.0
|
||||
concat-stream: 2.0.0
|
||||
http-response-object: 3.0.2
|
||||
parse-cache-control: 1.0.1
|
||||
|
||||
'@fastify/ajv-compiler@3.6.0':
|
||||
dependencies:
|
||||
ajv: 8.17.1
|
||||
@@ -406,12 +305,6 @@ snapshots:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
|
||||
'@types/fluent-ffmpeg@2.1.26':
|
||||
dependencies:
|
||||
'@types/node': 22.5.4
|
||||
|
||||
'@types/node@10.17.60': {}
|
||||
|
||||
'@types/node@22.5.4':
|
||||
dependencies:
|
||||
undici-types: 6.19.8
|
||||
@@ -422,12 +315,6 @@ snapshots:
|
||||
|
||||
abstract-logging@2.0.1: {}
|
||||
|
||||
agent-base@6.0.2:
|
||||
dependencies:
|
||||
debug: 4.3.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
ajv-formats@2.1.1(ajv@8.17.1):
|
||||
optionalDependencies:
|
||||
ajv: 8.17.1
|
||||
@@ -443,8 +330,6 @@ snapshots:
|
||||
json-schema-traverse: 1.0.0
|
||||
require-from-string: 2.0.2
|
||||
|
||||
async@0.2.10: {}
|
||||
|
||||
atomic-sleep@1.0.0: {}
|
||||
|
||||
avvio@8.4.0:
|
||||
@@ -454,38 +339,21 @@ snapshots:
|
||||
|
||||
base64-js@1.5.1: {}
|
||||
|
||||
buffer-from@1.1.2: {}
|
||||
|
||||
buffer@6.0.3:
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
ieee754: 1.2.1
|
||||
|
||||
caseless@0.12.0: {}
|
||||
|
||||
colorette@2.0.20: {}
|
||||
|
||||
concat-stream@2.0.0:
|
||||
dependencies:
|
||||
buffer-from: 1.1.2
|
||||
inherits: 2.0.4
|
||||
readable-stream: 3.6.2
|
||||
typedarray: 0.0.6
|
||||
|
||||
cookie@0.6.0: {}
|
||||
|
||||
dateformat@4.6.3: {}
|
||||
|
||||
debug@4.3.7:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
end-of-stream@1.4.4:
|
||||
dependencies:
|
||||
once: 1.4.0
|
||||
|
||||
env-paths@2.2.1: {}
|
||||
|
||||
event-target-shim@5.0.1: {}
|
||||
|
||||
events@3.3.0: {}
|
||||
@@ -543,49 +411,20 @@ snapshots:
|
||||
dependencies:
|
||||
reusify: 1.0.4
|
||||
|
||||
ffmpeg-static@5.2.0:
|
||||
dependencies:
|
||||
'@derhuerst/http-basic': 8.2.4
|
||||
env-paths: 2.2.1
|
||||
https-proxy-agent: 5.0.1
|
||||
progress: 2.0.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
find-my-way@8.2.0:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
fast-querystring: 1.1.2
|
||||
safe-regex2: 3.1.0
|
||||
|
||||
fluent-ffmpeg@2.1.3:
|
||||
dependencies:
|
||||
async: 0.2.10
|
||||
which: 1.3.1
|
||||
|
||||
forwarded@0.2.0: {}
|
||||
|
||||
help-me@5.0.0: {}
|
||||
|
||||
http-response-object@3.0.2:
|
||||
dependencies:
|
||||
'@types/node': 10.17.60
|
||||
|
||||
https-proxy-agent@5.0.1:
|
||||
dependencies:
|
||||
agent-base: 6.0.2
|
||||
debug: 4.3.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
ieee754@1.2.1: {}
|
||||
|
||||
inherits@2.0.4: {}
|
||||
|
||||
ipaddr.js@1.9.1: {}
|
||||
|
||||
isexe@2.0.0: {}
|
||||
|
||||
joycon@3.1.1: {}
|
||||
|
||||
json-schema-ref-resolver@1.0.1:
|
||||
@@ -602,16 +441,12 @@ snapshots:
|
||||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
on-exit-leak-free@2.1.2: {}
|
||||
|
||||
once@1.4.0:
|
||||
dependencies:
|
||||
wrappy: 1.0.2
|
||||
|
||||
parse-cache-control@1.0.1: {}
|
||||
|
||||
pino-abstract-transport@1.2.0:
|
||||
dependencies:
|
||||
readable-stream: 4.5.2
|
||||
@@ -656,8 +491,6 @@ snapshots:
|
||||
|
||||
process@0.11.10: {}
|
||||
|
||||
progress@2.0.3: {}
|
||||
|
||||
proxy-addr@2.0.7:
|
||||
dependencies:
|
||||
forwarded: 0.2.0
|
||||
@@ -670,12 +503,6 @@ snapshots:
|
||||
|
||||
quick-format-unescaped@4.0.4: {}
|
||||
|
||||
readable-stream@3.6.2:
|
||||
dependencies:
|
||||
inherits: 2.0.4
|
||||
string_decoder: 1.3.0
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
readable-stream@4.5.2:
|
||||
dependencies:
|
||||
abort-controller: 3.0.0
|
||||
@@ -726,16 +553,8 @@ snapshots:
|
||||
|
||||
toad-cache@3.7.0: {}
|
||||
|
||||
typedarray@0.0.6: {}
|
||||
|
||||
typescript@5.6.2: {}
|
||||
|
||||
undici-types@6.19.8: {}
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
which@1.3.1:
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
|
||||
wrappy@1.0.2: {}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import fastify from "fastify";
|
||||
import { tsToMpeg4 } from "./utils";
|
||||
import { bufferVideo } from "./utils";
|
||||
|
||||
const envToLogger = {
|
||||
development: {
|
||||
@@ -26,14 +26,19 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
const { NODE_ENV: env = "development" } = process.env;
|
||||
const { NODE_ENV: env = "development", PORT } = process.env;
|
||||
|
||||
const app = fastify({ logger: envToLogger[env] });
|
||||
|
||||
app.addContentTypeParser("*", (_, __, done) => done(null));
|
||||
|
||||
app.get("/", async (_, res) => res.redirect("https://bskyx.app"));
|
||||
|
||||
// serve 0 bytes favicon so browsers don't spam the server
|
||||
app.get("/favicon.ico", (_, res) => res.send(""));
|
||||
|
||||
app.get<{ Params: { "*": string } }>(
|
||||
"/generate/*",
|
||||
"/*",
|
||||
{
|
||||
schema: {
|
||||
params: {
|
||||
@@ -46,41 +51,27 @@ app.get<{ Params: { "*": string } }>(
|
||||
},
|
||||
},
|
||||
async (req, res) => {
|
||||
// Idk anymore
|
||||
let urls = Buffer.from(req.params["*"], 'base64').toString().split(";");
|
||||
const payload = Buffer.from(req.params["*"], "base64")
|
||||
.toString()
|
||||
.split(";");
|
||||
|
||||
// Remove .mp4 extension if it exists
|
||||
if (urls.at(-1)?.endsWith(".mp4")) {
|
||||
urls = urls.slice(0, -1);
|
||||
urls.push(urls.at(-1)?.slice(0, -4) as string);
|
||||
}
|
||||
const [did, id, quality] = payload;
|
||||
|
||||
urls = urls.map((url) => decodeURIComponent(url)).map((url) => url.replaceAll('\uFFFD', ''));
|
||||
const url = `https://video.bsky.app/watch/${did}/${id}/${quality}/video.m3u8`;
|
||||
|
||||
const result = await Promise.allSettled(
|
||||
urls.map((url) => fetch(url).then((res) => res.arrayBuffer()))
|
||||
);
|
||||
try {
|
||||
bufferVideo(url, res);
|
||||
|
||||
if (result.some((res) => res.status === "rejected")) {
|
||||
res.header("Content-Type", "video/mp4");
|
||||
res.header("Cache-Control", "public, max-age=604800");
|
||||
return res;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.status(400).send({ error: "Failed to fetch video" });
|
||||
return;
|
||||
}
|
||||
|
||||
const buffers = result.map((res) =>
|
||||
res.status === "fulfilled" ? new Uint8Array(res.value) : new Uint8Array(0)
|
||||
);
|
||||
|
||||
const video = await tsToMpeg4(buffers);
|
||||
|
||||
// const fileName = `video-${new Date().toUTCString()}.mp4`;
|
||||
|
||||
res.header("Content-Type", "video/mp4");
|
||||
res.header("Cache-Control", "public, max-age=604800");
|
||||
// res.header("Content-Disposition", `attachment; filename=${fileName}`);
|
||||
res.send(video);
|
||||
}
|
||||
);
|
||||
|
||||
const port = process.env.PORT ? Number(process.env.PORT) : 3000;
|
||||
const port = PORT ? Number(PORT) : 3000;
|
||||
|
||||
app.listen({ port, host: "0.0.0.0" });
|
||||
|
||||
@@ -1,52 +1,53 @@
|
||||
import ffmpeg from "fluent-ffmpeg";
|
||||
import ffmpegPath from "ffmpeg-static";
|
||||
import { type ChildProcess, spawn as s } from "node:child_process";
|
||||
|
||||
import { PassThrough } from "node:stream";
|
||||
import fs from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { type FastifyReply } from "fastify";
|
||||
import type { Writable, Readable } from "node:stream";
|
||||
|
||||
ffmpeg.setFfmpegPath(ffmpegPath as string);
|
||||
export function bufferVideo(masterUrl: string, res: FastifyReply) {
|
||||
let process: ChildProcess;
|
||||
const cleanup = () => {
|
||||
process?.kill("SIGTERM");
|
||||
setTimeout(() => process?.kill("SIGKILL"), 5000);
|
||||
res.raw.end();
|
||||
};
|
||||
|
||||
export function tsToMpeg4(buffers: Uint8Array[]): Promise<Buffer> {
|
||||
return new Promise((res, rej) => {
|
||||
const input = new PassThrough();
|
||||
const args = [
|
||||
"-loglevel",
|
||||
"-8",
|
||||
"-i",
|
||||
masterUrl,
|
||||
"-c:v",
|
||||
"copy",
|
||||
"-c:a",
|
||||
"aac",
|
||||
"-bsf:a",
|
||||
"aac_adtstoasc",
|
||||
"-movflags",
|
||||
"faststart+frag_keyframe+empty_moov",
|
||||
"-f",
|
||||
"mp4",
|
||||
"pipe:3",
|
||||
];
|
||||
|
||||
buffers.forEach((b) => input.write(b));
|
||||
|
||||
input.end();
|
||||
|
||||
const tempFilePath = path.join(tmpdir(), `output-${Date.now()}.mp4`);
|
||||
|
||||
ffmpeg(input)
|
||||
.outputOption(
|
||||
"-c",
|
||||
"copy",
|
||||
"-movflags",
|
||||
"faststart",
|
||||
"-preset",
|
||||
"ultrafast"
|
||||
)
|
||||
.on("end", async () => {
|
||||
try {
|
||||
const ob = await fs.readFile(tempFilePath);
|
||||
await fs.unlink(tempFilePath);
|
||||
res(ob);
|
||||
} catch (e) {
|
||||
rej(e);
|
||||
}
|
||||
})
|
||||
.on("error", async (err, stdout, stderr) => {
|
||||
console.error("Error:", err.message);
|
||||
console.error("ffmpeg stdout:", stdout);
|
||||
console.error("ffmpeg stderr:", stderr);
|
||||
try {
|
||||
await fs.unlink(tempFilePath);
|
||||
} catch (e) {
|
||||
rej(e);
|
||||
}
|
||||
rej(err);
|
||||
})
|
||||
.save(tempFilePath);
|
||||
process = s("ffmpeg", args, {
|
||||
windowsHide: true,
|
||||
stdio: ["inherit", "inherit", "inherit", "pipe"],
|
||||
});
|
||||
|
||||
const [, , , stream] = process.stdio;
|
||||
pipe(stream, res.raw, cleanup);
|
||||
process.on("close", cleanup);
|
||||
process.on("exit", cleanup);
|
||||
res.raw.on("finish", cleanup);
|
||||
return stream;
|
||||
}
|
||||
|
||||
function pipe(
|
||||
from: Readable | Writable | undefined | null,
|
||||
to: NodeJS.WritableStream,
|
||||
cleanup: () => void
|
||||
) {
|
||||
from?.on("error", cleanup).on("close", cleanup);
|
||||
to.on("error", cleanup).on("close", cleanup);
|
||||
from?.pipe(to);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { OEmbedTypes } from "../routes/getOEmbed";
|
||||
import { parseEmbedImages } from "../lib/parseEmbedImages";
|
||||
import { parseEmbedDescription } from "../lib/parseEmbedDescription";
|
||||
import { StreamInfo } from "../lib/processVideoEmbed";
|
||||
import { join } from "../lib/utils";
|
||||
import { checkType, join } from "../lib/utils";
|
||||
|
||||
interface PostProps {
|
||||
post: AppBskyFeedDefs.PostView;
|
||||
@@ -21,6 +21,16 @@ const Meta = ({ post }: { post: AppBskyFeedDefs.PostView }) => (
|
||||
</>
|
||||
);
|
||||
|
||||
const constructVideoUrl = (streamInfo: StreamInfo, apiUrl: string) => {
|
||||
const url = new URL(streamInfo.masterUri);
|
||||
|
||||
const [did, id, quality] = url.pathname.split("/").slice(2);
|
||||
|
||||
const parts = [did, id, quality];
|
||||
|
||||
return `${apiUrl}generate/${btoa(join(parts, ";"))}.mp4`;
|
||||
};
|
||||
|
||||
const Video = ({
|
||||
streamInfo,
|
||||
apiUrl,
|
||||
@@ -34,8 +44,7 @@ const Video = ({
|
||||
post: AppBskyFeedDefs.PostView;
|
||||
description: string;
|
||||
}) => {
|
||||
// Discord can't handle query params in the URL, so i have to do this 🔥beautiful mess🔥
|
||||
const url = `${apiUrl}generate/${btoa(join(streamInfo.uri, ";"))}.mp4`;
|
||||
const url = constructVideoUrl(streamInfo, apiUrl);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -109,7 +118,21 @@ export const Post = ({
|
||||
}: PostProps) => {
|
||||
const images = parseEmbedImages(post);
|
||||
const isAuthor = images === post.author.avatar;
|
||||
const description = parseEmbedDescription(post);
|
||||
let description = parseEmbedDescription(post);
|
||||
const isVideo = checkType(
|
||||
"app.bsky.embed.video",
|
||||
post.embed?.media ?? post.embed
|
||||
);
|
||||
const streamInfo = videoMetadata?.at(-1);
|
||||
const isTooLong = isVideo && streamInfo!.uri.length > 4;
|
||||
const shouldOverrideForVideo = isVideo && isTooLong;
|
||||
|
||||
let videoUrl;
|
||||
|
||||
if (isVideo && isTooLong) {
|
||||
videoUrl = constructVideoUrl(streamInfo!, apiUrl);
|
||||
description += `\n[Video is too long to embed!]`;
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout url={url}>
|
||||
@@ -125,23 +148,24 @@ export const Post = ({
|
||||
/>
|
||||
<meta property="og:updated_time" content={post.indexedAt} />
|
||||
<meta property="article:published_time" content={post.indexedAt} />
|
||||
{/* <meta property="og:image" content={post.author.avatar} /> */}
|
||||
|
||||
{!isAuthor && <Meta post={post} />}
|
||||
|
||||
{images.length !== 0 && !videoMetadata && <Images images={images} />}
|
||||
{images.length !== 0 && (shouldOverrideForVideo || !isVideo) && (
|
||||
<Images images={images} />
|
||||
)}
|
||||
|
||||
{videoMetadata && (
|
||||
{isVideo && streamInfo!.uri.length <= 4 && (
|
||||
<Video
|
||||
apiUrl={apiUrl}
|
||||
streamInfo={videoMetadata.at(-1)!}
|
||||
streamInfo={streamInfo!}
|
||||
appDomain={appDomain}
|
||||
description={description}
|
||||
post={post}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!videoMetadata && (
|
||||
{(shouldOverrideForVideo || !isVideo) && (
|
||||
<link
|
||||
rel="alternate"
|
||||
type="application/json+oembed"
|
||||
@@ -151,7 +175,9 @@ export const Post = ({
|
||||
post.likeCount
|
||||
}&avatar=${encodeURIComponent(
|
||||
post.author.avatar ?? ""
|
||||
)}&description=${encodeURIComponent(description)}`}
|
||||
)}&description=${encodeURIComponent(description)}${
|
||||
videoUrl ? `&videoUrl=${encodeURIComponent(videoUrl)}` : ""
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</Layout>
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
AppBskyFeedDefs,
|
||||
AppBskyFeedPost,
|
||||
} from "@atproto/api";
|
||||
import { indent } from "./utils";
|
||||
|
||||
export function parseEmbedDescription(post: AppBskyFeedDefs.PostView) {
|
||||
if (AppBskyFeedPost.isRecord(post.record)) {
|
||||
@@ -15,7 +16,7 @@ export function parseEmbedDescription(post: AppBskyFeedDefs.PostView) {
|
||||
);
|
||||
if (isViewRecord) {
|
||||
// @ts-expect-error For some reason the original post value is typed as {}
|
||||
return `${post.record.text}\n\nQuoting @${post.embed.record.author.handle}\n➥ ${post.embed.record.value.text}`;
|
||||
return `${post.record.text}\n\nQuoting @${post.embed.record.author.handle}\n➥${indent(post.embed.record.value.text, 2)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,7 +30,7 @@ export function parseEmbedDescription(post: AppBskyFeedDefs.PostView) {
|
||||
);
|
||||
if (isViewRecord) {
|
||||
// @ts-expect-error For some reason the original post value is typed as {}
|
||||
return `${post.record.text}\n\nQuoting @${post.embed.record.record.author.handle}\n➥ ${post.embed.record.record.value.text}`;
|
||||
return `${post.record.text}\n\nQuoting @${post.embed.record.record.author.handle}\n➥${indent(post.embed.record.record.value.text, 2)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface StreamInfo {
|
||||
};
|
||||
codecs: string;
|
||||
uri: string | string[];
|
||||
masterUri: string;
|
||||
}
|
||||
|
||||
export interface M3U8Data {
|
||||
@@ -66,6 +67,7 @@ async function parseM3U8(
|
||||
},
|
||||
codecs: "",
|
||||
uri: "",
|
||||
masterUri: "",
|
||||
};
|
||||
|
||||
for (const attrib of attribs) {
|
||||
@@ -88,6 +90,8 @@ async function parseM3U8(
|
||||
} else if (line.includes("m3u8")) {
|
||||
const resolvedUrl = `${initalUrl}/${line}`;
|
||||
|
||||
streams.at(-1)!.masterUri = resolvedUrl;
|
||||
|
||||
const cont = await fetch(resolvedUrl).then((res) => res.text());
|
||||
|
||||
const parsed = await parseM3U8(removeLastPathSegment(resolvedUrl), cont);
|
||||
@@ -102,6 +106,7 @@ async function parseM3U8(
|
||||
},
|
||||
codecs: "",
|
||||
uri: `${initalUrl}/${line}`,
|
||||
masterUri: initalUrl,
|
||||
});
|
||||
} else {
|
||||
// Discard
|
||||
|
||||
@@ -12,4 +12,10 @@ export const join = (t: string | string[], s: string) =>
|
||||
Array.isArray(t) ? t.join(s) : t;
|
||||
|
||||
export const checkType = (t: string, o: any) =>
|
||||
(typeof o.$type === "string" && o.$type.startsWith(t)) || o.$type === t;
|
||||
(typeof o?.$type === "string" && o?.$type.startsWith(t)) || o?.$type === t;
|
||||
|
||||
export const indent = (s: string, n: number) =>
|
||||
s
|
||||
.split("\n")
|
||||
.map((l) => " ".repeat(n) + l)
|
||||
.join("\n");
|
||||
|
||||
@@ -23,11 +23,12 @@ export const getOEmbed: Handler<Env, "/oembed"> = async (c) => {
|
||||
}
|
||||
|
||||
if (type === OEmbedTypes.Post) {
|
||||
const { replies, reposts, likes } = c.req.query();
|
||||
const { replies, reposts, likes, videoUrl } = c.req.query();
|
||||
|
||||
return c.json({
|
||||
author_name: `🗨️ ${replies} ♻️ ${reposts} 💙 ${likes}`,
|
||||
...defaults,
|
||||
author_name: `🗨️ ${replies} ♻️ ${reposts} 💙 ${likes}`,
|
||||
provider_url: videoUrl ? videoUrl : defaults.provider_url,
|
||||
});
|
||||
}
|
||||
if (type === OEmbedTypes.Profile) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Handler } from "hono";
|
||||
import { HTTPException } from "hono/http-exception";
|
||||
import { fetchPost } from "../lib/fetchPostData";
|
||||
import { Post } from "../components/Post";
|
||||
import { processVideoEmbed, StreamInfo, VideoMedia } from "../lib/processVideoEmbed";
|
||||
import { processVideoEmbed, StreamInfo } from "../lib/processVideoEmbed";
|
||||
import { checkType } from "../lib/utils";
|
||||
|
||||
export const getPost: Handler<
|
||||
@@ -27,8 +27,10 @@ export const getPost: Handler<
|
||||
checkType("app.bsky.embed.video", fetchedPost.embed) ||
|
||||
checkType("app.bsky.embed.video", fetchedPost.embed?.media)
|
||||
) {
|
||||
// @ts-expect-error
|
||||
videoMetaData = await processVideoEmbed(fetchedPost.embed?.media || fetchedPost.embed);
|
||||
videoMetaData = await processVideoEmbed(
|
||||
// @ts-expect-error
|
||||
fetchedPost.embed?.media ?? fetchedPost.embed
|
||||
);
|
||||
}
|
||||
|
||||
return c.html(
|
||||
|
||||
Reference in New Issue
Block a user