feat: test publish to youtube

This commit is contained in:
2025-11-04 10:20:09 +01:00
parent 1adf5ba5e4
commit e0babe90c0
5 changed files with 322 additions and 34 deletions
+6
View File
@@ -0,0 +1,6 @@
YOUTUBE_CLIENT_ID=your-youtube-client-id
YOUTUBE_CLIENT_SECRET=your-youtube-client-secret
YOUTUBE_REFRESH_TOKEN=your-youtube-refresh-token
# YOUTUBE_REDIRECT_URI=http://localhost:8081
# YOUTUBE_PRIVACY_STATUS=unlisted
# YOUTUBE_CATEGORY_ID=10
+3
View File
@@ -1,3 +1,5 @@
require("dotenv").config();
const admin = require("firebase-admin"); const admin = require("firebase-admin");
// Use default credentials/environment provided by Cloud Functions. // Use default credentials/environment provided by Cloud Functions.
@@ -45,3 +47,4 @@ exports.algolia = require("./src/algolia");
exports.notifications = require("./src/notifications"); exports.notifications = require("./src/notifications");
exports.rankings = require("./src/rankings"); exports.rankings = require("./src/rankings");
exports.payouts = require("./src/payouts"); exports.payouts = require("./src/payouts");
exports.youtube = require("./src/youtube");
+34 -34
View File
@@ -8,14 +8,17 @@
"dependencies": { "dependencies": {
"@ffmpeg-installer/ffmpeg": "^1.1.0", "@ffmpeg-installer/ffmpeg": "^1.1.0",
"@genkit-ai/googleai": "^1.18.0", "@genkit-ai/googleai": "^1.18.0",
"@google-cloud/firestore": "^7.11.6",
"@google-cloud/storage": "^7.17.1", "@google-cloud/storage": "^7.17.1",
"axios": "^1.11.0", "axios": "^1.11.0",
"dotenv": "^17.2.3",
"expo-server-sdk": "^4.0.0", "expo-server-sdk": "^4.0.0",
"ffmpeg-static": "^5.2.0", "ffmpeg-static": "^5.2.0",
"firebase-admin": "^12.6.0", "firebase-admin": "^12.6.0",
"firebase-functions": "^6.0.1", "firebase-functions": "^6.0.1",
"fluent-ffmpeg": "^2.1.3", "fluent-ffmpeg": "^2.1.3",
"genkit": "^1.18.0", "genkit": "^1.18.0",
"googleapis": "^131.0.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"resend": "^6.3.0", "resend": "^6.3.0",
"sharp": "^0.33.4" "sharp": "^0.33.4"
@@ -536,9 +539,6 @@
"version": "7.11.6", "version": "7.11.6",
"resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.11.6.tgz", "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.11.6.tgz",
"integrity": "sha512-EW/O8ktzwLfyWBOsNuhRoMi8lrC3clHM5LVFhGvO1HCsLozCOOXRAlHrYBoE6HL42Sc8yYMuCb2XqcnJ4OOEpw==", "integrity": "sha512-EW/O8ktzwLfyWBOsNuhRoMi8lrC3clHM5LVFhGvO1HCsLozCOOXRAlHrYBoE6HL42Sc8yYMuCb2XqcnJ4OOEpw==",
"license": "Apache-2.0",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"@opentelemetry/api": "^1.3.0", "@opentelemetry/api": "^1.3.0",
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.1",
@@ -633,6 +633,20 @@
"@opentelemetry/sdk-metrics": "^1.0.0" "@opentelemetry/sdk-metrics": "^1.0.0"
} }
}, },
"node_modules/@google-cloud/opentelemetry-cloud-monitoring-exporter/node_modules/googleapis": {
"version": "137.1.0",
"resolved": "https://registry.npmjs.org/googleapis/-/googleapis-137.1.0.tgz",
"integrity": "sha512-2L7SzN0FLHyQtFmyIxrcXhgust77067pkkduqkbIpDuj9JzVnByxsRrcRfUMFQam3rQkWW2B0f1i40IwKDWIVQ==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"google-auth-library": "^9.0.0",
"googleapis-common": "^7.0.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@google-cloud/opentelemetry-cloud-trace-exporter": { "node_modules/@google-cloud/opentelemetry-cloud-trace-exporter": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/@google-cloud/opentelemetry-cloud-trace-exporter/-/opentelemetry-cloud-trace-exporter-2.4.1.tgz", "resolved": "https://registry.npmjs.org/@google-cloud/opentelemetry-cloud-trace-exporter/-/opentelemetry-cloud-trace-exporter-2.4.1.tgz",
@@ -784,7 +798,6 @@
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz",
"integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true,
"dependencies": { "dependencies": {
"lodash.camelcase": "^4.3.0", "lodash.camelcase": "^4.3.0",
"long": "^5.0.0", "long": "^5.0.0",
@@ -1250,7 +1263,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"engines": { "engines": {
"node": ">=8.0.0" "node": ">=8.0.0"
} }
@@ -1346,7 +1358,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz",
"integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@opentelemetry/semantic-conventions": "1.25.1" "@opentelemetry/semantic-conventions": "1.25.1"
}, },
@@ -2534,7 +2545,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.25.1.tgz", "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.25.1.tgz",
"integrity": "sha512-pkZT+iFYIZsVn6+GzM0kSX+u3MSLCY9md+lIJOoKl/P+gJFfxJte/60Usdp8Ce4rOs8GduUpSPNe1ddGyDT1sQ==", "integrity": "sha512-pkZT+iFYIZsVn6+GzM0kSX+u3MSLCY9md+lIJOoKl/P+gJFfxJte/60Usdp8Ce4rOs8GduUpSPNe1ddGyDT1sQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@opentelemetry/core": "1.25.1", "@opentelemetry/core": "1.25.1",
"@opentelemetry/semantic-conventions": "1.25.1" "@opentelemetry/semantic-conventions": "1.25.1"
@@ -2577,7 +2587,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.25.1.tgz", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.25.1.tgz",
"integrity": "sha512-9Mb7q5ioFL4E4dDrc4wC/A3NTHDat44v4I3p2pLPSxRvqUbDIQyMVr9uK+EU69+HWhlET1VaSrRzwdckWqY15Q==", "integrity": "sha512-9Mb7q5ioFL4E4dDrc4wC/A3NTHDat44v4I3p2pLPSxRvqUbDIQyMVr9uK+EU69+HWhlET1VaSrRzwdckWqY15Q==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@opentelemetry/core": "1.25.1", "@opentelemetry/core": "1.25.1",
"@opentelemetry/resources": "1.25.1", "@opentelemetry/resources": "1.25.1",
@@ -2631,7 +2640,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.25.1.tgz", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.25.1.tgz",
"integrity": "sha512-C8k4hnEbc5FamuZQ92nTOp8X/diCY56XUTnMiv9UTuJitCzaNNHAVsdm5+HLCdI8SLQsLWIrG38tddMxLVoftw==", "integrity": "sha512-C8k4hnEbc5FamuZQ92nTOp8X/diCY56XUTnMiv9UTuJitCzaNNHAVsdm5+HLCdI8SLQsLWIrG38tddMxLVoftw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@opentelemetry/core": "1.25.1", "@opentelemetry/core": "1.25.1",
"@opentelemetry/resources": "1.25.1", "@opentelemetry/resources": "1.25.1",
@@ -2940,8 +2948,7 @@
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
"integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==",
"license": "MIT", "license": "MIT"
"optional": true
}, },
"node_modules/@types/memcached": { "node_modules/@types/memcached": {
"version": "2.2.10", "version": "2.2.10",
@@ -3143,7 +3150,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -3779,6 +3785,17 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/dotenv": {
"version": "17.2.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dotprompt": { "node_modules/dotprompt": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/dotprompt/-/dotprompt-1.1.2.tgz", "resolved": "https://registry.npmjs.org/dotprompt/-/dotprompt-1.1.2.tgz",
@@ -3971,7 +3988,6 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1", "@eslint-community/regexpp": "^4.6.1",
@@ -4466,7 +4482,6 @@
"resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-12.7.0.tgz", "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-12.7.0.tgz",
"integrity": "sha512-raFIrOyTqREbyXsNkSHyciQLfv8AUZazehPaQS1lZBSCDYW74FYXU0nQZa3qHI4K+hawohlDbywZ4+qce9YNxA==", "integrity": "sha512-raFIrOyTqREbyXsNkSHyciQLfv8AUZazehPaQS1lZBSCDYW74FYXU0nQZa3qHI4K+hawohlDbywZ4+qce9YNxA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@fastify/busboy": "^3.0.0", "@fastify/busboy": "^3.0.0",
"@firebase/database-compat": "1.0.8", "@firebase/database-compat": "1.0.8",
@@ -4662,8 +4677,7 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
"integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==",
"license": "MIT", "license": "MIT"
"optional": true
}, },
"node_modules/gaxios": { "node_modules/gaxios": {
"version": "6.7.1", "version": "6.7.1",
@@ -4755,7 +4769,6 @@
"resolved": "https://registry.npmjs.org/genkit/-/genkit-1.22.0.tgz", "resolved": "https://registry.npmjs.org/genkit/-/genkit-1.22.0.tgz",
"integrity": "sha512-GoVVO3EnNHrjkMkUPRvgx1MjBHKvOlZAu/ffMIJgLFxrH7rrUbvfHXE6Nk7uh5BNvET7+DApyhbhqz9G8sy+mQ==", "integrity": "sha512-GoVVO3EnNHrjkMkUPRvgx1MjBHKvOlZAu/ffMIJgLFxrH7rrUbvfHXE6Nk7uh5BNvET7+DApyhbhqz9G8sy+mQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"@genkit-ai/ai": "1.22.0", "@genkit-ai/ai": "1.22.0",
"@genkit-ai/core": "1.22.0", "@genkit-ai/core": "1.22.0",
@@ -4906,7 +4919,6 @@
"resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.6.1.tgz", "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.6.1.tgz",
"integrity": "sha512-V6eky/xz2mcKfAd1Ioxyd6nmA61gao3n01C+YeuIwu3vzM9EDR6wcVzMSIbLMDXWeoi9SHYctXuKYC5uJUT3eQ==", "integrity": "sha512-V6eky/xz2mcKfAd1Ioxyd6nmA61gao3n01C+YeuIwu3vzM9EDR6wcVzMSIbLMDXWeoi9SHYctXuKYC5uJUT3eQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true,
"dependencies": { "dependencies": {
"@grpc/grpc-js": "^1.10.9", "@grpc/grpc-js": "^1.10.9",
"@grpc/proto-loader": "^0.7.13", "@grpc/proto-loader": "^0.7.13",
@@ -4930,7 +4942,6 @@
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT", "license": "MIT",
"optional": true,
"dependencies": { "dependencies": {
"whatwg-url": "^5.0.0" "whatwg-url": "^5.0.0"
}, },
@@ -4955,7 +4966,6 @@
"https://github.com/sponsors/ctavan" "https://github.com/sponsors/ctavan"
], ],
"license": "MIT", "license": "MIT",
"optional": true,
"bin": { "bin": {
"uuid": "dist/bin/uuid" "uuid": "dist/bin/uuid"
} }
@@ -4970,11 +4980,10 @@
} }
}, },
"node_modules/googleapis": { "node_modules/googleapis": {
"version": "137.1.0", "version": "131.0.0",
"resolved": "https://registry.npmjs.org/googleapis/-/googleapis-137.1.0.tgz", "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-131.0.0.tgz",
"integrity": "sha512-2L7SzN0FLHyQtFmyIxrcXhgust77067pkkduqkbIpDuj9JzVnByxsRrcRfUMFQam3rQkWW2B0f1i40IwKDWIVQ==", "integrity": "sha512-fa4kdkY0VwHDw/04ItpQv2tlvlPIwbh6NjHDoWAVrV52GuaZbYCMOC5Y+hRmprp5HHIMRODmyb2YujlbZSRUbQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true,
"dependencies": { "dependencies": {
"google-auth-library": "^9.0.0", "google-auth-library": "^9.0.0",
"googleapis-common": "^7.0.0" "googleapis-common": "^7.0.0"
@@ -4988,7 +4997,6 @@
"resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.2.0.tgz", "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.2.0.tgz",
"integrity": "sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==", "integrity": "sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true,
"dependencies": { "dependencies": {
"extend": "^3.0.2", "extend": "^3.0.2",
"gaxios": "^6.0.3", "gaxios": "^6.0.3",
@@ -5010,7 +5018,6 @@
"https://github.com/sponsors/ctavan" "https://github.com/sponsors/ctavan"
], ],
"license": "MIT", "license": "MIT",
"optional": true,
"bin": { "bin": {
"uuid": "dist/bin/uuid" "uuid": "dist/bin/uuid"
} }
@@ -5923,7 +5930,6 @@
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
"license": "MIT", "license": "MIT",
"optional": true,
"engines": { "engines": {
"node": ">= 6" "node": ">= 6"
} }
@@ -6241,7 +6247,6 @@
"resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz",
"integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==", "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true,
"dependencies": { "dependencies": {
"protobufjs": "^7.2.5" "protobufjs": "^7.2.5"
}, },
@@ -7234,8 +7239,7 @@
"version": "2.0.8", "version": "2.0.8",
"resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz",
"integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==",
"license": "BSD", "license": "BSD"
"optional": true
}, },
"node_modules/util-deprecate": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
@@ -7338,9 +7342,7 @@
"version": "3.18.3", "version": "3.18.3",
"resolved": "https://registry.npmjs.org/winston/-/winston-3.18.3.tgz", "resolved": "https://registry.npmjs.org/winston/-/winston-3.18.3.tgz",
"integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==", "integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==",
"license": "MIT",
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"@colors/colors": "^1.6.0", "@colors/colors": "^1.6.0",
"@dabh/diagnostics": "^2.0.8", "@dabh/diagnostics": "^2.0.8",
@@ -7377,7 +7379,6 @@
"version": "3.2.6", "version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
"license": "MIT",
"optional": true "optional": true
}, },
"node_modules/word-wrap": { "node_modules/word-wrap": {
@@ -7505,7 +7506,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }
+3
View File
@@ -16,14 +16,17 @@
"dependencies": { "dependencies": {
"@ffmpeg-installer/ffmpeg": "^1.1.0", "@ffmpeg-installer/ffmpeg": "^1.1.0",
"@genkit-ai/googleai": "^1.18.0", "@genkit-ai/googleai": "^1.18.0",
"@google-cloud/firestore": "^7.11.6",
"@google-cloud/storage": "^7.17.1", "@google-cloud/storage": "^7.17.1",
"axios": "^1.11.0", "axios": "^1.11.0",
"dotenv": "^17.2.3",
"expo-server-sdk": "^4.0.0", "expo-server-sdk": "^4.0.0",
"ffmpeg-static": "^5.2.0", "ffmpeg-static": "^5.2.0",
"firebase-admin": "^12.6.0", "firebase-admin": "^12.6.0",
"firebase-functions": "^6.0.1", "firebase-functions": "^6.0.1",
"fluent-ffmpeg": "^2.1.3", "fluent-ffmpeg": "^2.1.3",
"genkit": "^1.18.0", "genkit": "^1.18.0",
"googleapis": "^131.0.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"resend": "^6.3.0", "resend": "^6.3.0",
"sharp": "^0.33.4" "sharp": "^0.33.4"
+276
View File
@@ -0,0 +1,276 @@
const { onCall, HttpsError } = require("firebase-functions/v2/https");
const functions = require("firebase-functions");
const logger = require("firebase-functions/logger");
const admin = require("firebase-admin");
const axios = require("axios");
const fs = require("node:fs");
const fsp = require("node:fs/promises");
const os = require("node:os");
const path = require("node:path");
const { google } = require("googleapis");
const firestore = admin.firestore();
const projectsRef = firestore.collection("projects");
const YOUTUBE_IN_PROGRESS_STATUSES = [
"PUBLISHING",
"UPLOADING",
"PROCESSING",
"QUEUED",
];
const getEnvYoutubeConfig = () =>
Object.fromEntries(
Object.entries({
client_id: process.env.YOUTUBE_CLIENT_ID,
client_secret: process.env.YOUTUBE_CLIENT_SECRET,
refresh_token: process.env.YOUTUBE_REFRESH_TOKEN,
redirect_uri: process.env.YOUTUBE_REDIRECT_URI,
privacy_status: process.env.YOUTUBE_PRIVACY_STATUS,
category_id: process.env.YOUTUBE_CATEGORY_ID,
}).filter(([, value]) => value !== undefined && value !== "")
);
const ensureYoutubeConfig = () => {
const firebaseConfig = functions.config()?.youtube || {};
const envConfig = getEnvYoutubeConfig();
const cfg = { ...firebaseConfig, ...envConfig };
const requiredKeys = ["client_id", "client_secret", "refresh_token"];
const missing = requiredKeys.filter((key) => !cfg[key]);
if (missing.length) {
throw new HttpsError(
"failed-precondition",
`Configuration YouTube manquante: ${missing.join(", ")}`
);
}
return {
clientId: cfg.client_id,
clientSecret: cfg.client_secret,
refreshToken: cfg.refresh_token,
redirectUri:
cfg.redirect_uri || "https://developers.google.com/oauthplayground",
defaultPrivacyStatus: cfg.privacy_status || "unlisted",
defaultCategoryId: cfg.category_id || "10",
};
};
const createYoutubeClient = ({
clientId,
clientSecret,
refreshToken,
redirectUri,
}) => {
const oauth2Client = new google.auth.OAuth2(
clientId,
clientSecret,
redirectUri
);
oauth2Client.setCredentials({ refresh_token: refreshToken });
const youtube = google.youtube({
version: "v3",
auth: oauth2Client,
});
return { youtube, oauth2Client };
};
const downloadFile = async (url, destinationPath) => {
if (!/^https?:\/\//i.test(url || "")) {
throw new HttpsError("invalid-argument", `URL non valide: ${url}`);
}
await fsp.mkdir(path.dirname(destinationPath), { recursive: true });
const response = await axios.get(url, { responseType: "stream" });
await new Promise((resolve, reject) => {
const writer = fs.createWriteStream(destinationPath);
response.data.pipe(writer);
writer.on("finish", resolve);
writer.on("error", reject);
});
return destinationPath;
};
const buildVideoMetadata = (project, defaults) => {
const baseTitle = project?.title || "Création MusicLand";
const youtubeTitle = `${baseTitle} | MusicLand`;
const description = `Vidéo générée avec MusicLand pour ${baseTitle}. Rejoins l'aventure sur l'app MusicLand !`;
const tags = Array.isArray(project?.youtubeTags)
? project.youtubeTags.filter(Boolean).slice(0, 500)
: undefined;
const snippet = {
title: youtubeTitle,
description,
categoryId: defaults.defaultCategoryId,
};
if (tags && tags.length) {
snippet.tags = tags;
}
return {
snippet,
status: {
privacyStatus: defaults.defaultPrivacyStatus,
embeddable: true,
selfDeclaredMadeForKids: false,
},
};
};
exports.publishPlaybackToYoutube = onCall(
{
timeoutSeconds: 540,
memory: "1GiB",
cors: [
"http://localhost:8081",
"https://musicland-one.vercel.app/",
"https://musicland-d33f9.firebaseapp.com",
],
},
async ({ data = {}, auth }) => {
const uid = auth?.uid;
if (!uid) {
throw new HttpsError("unauthenticated", "Authentification requise");
}
const projectId = data?.projectId;
if (!projectId || typeof projectId !== "string") {
throw new HttpsError("invalid-argument", "Paramètre projectId requis");
}
const projectSnap = await projectsRef.doc(projectId).get();
if (!projectSnap.exists) {
throw new HttpsError("not-found", "Projet introuvable");
}
const project = projectSnap.data();
if (!project || project.userId !== uid) {
throw new HttpsError(
"permission-denied",
"Vous n'avez pas les droits sur ce projet"
);
}
if (!project.playbackUrl) {
throw new HttpsError(
"failed-precondition",
"Aucun playback disponible pour la publication"
);
}
if (
project.youtubeStatus &&
YOUTUBE_IN_PROGRESS_STATUSES.includes(project.youtubeStatus)
) {
throw new HttpsError(
"failed-precondition",
"Une publication est déjà en cours pour ce projet"
);
}
let youtubeDefaults;
let youtubeClient;
try {
youtubeDefaults = ensureYoutubeConfig();
youtubeClient = createYoutubeClient(youtubeDefaults);
await youtubeClient.oauth2Client.getAccessToken();
} catch (error) {
logger.error("[publishPlaybackToYoutube] configuration invalide", {
error: error?.message,
});
throw new HttpsError(
"failed-precondition",
"Configuration YouTube invalide ou incomplète"
);
}
const tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), "yt-upload-"));
const videoPath = path.join(tmpDir, "playback.mp4");
try {
await projectsRef.doc(projectId).set(
{
youtubeStatus: "PUBLISHING",
youtubePublished: false,
youtubeError: null,
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
},
{ merge: true }
);
await downloadFile(project.playbackUrl, videoPath);
const metadata = buildVideoMetadata(project, youtubeDefaults);
const uploadResponse = await youtubeClient.youtube.videos.insert({
part: ["snippet", "status"].join(","),
requestBody: metadata,
media: {
body: fs.createReadStream(videoPath),
},
});
const videoId = uploadResponse?.data?.id;
if (!videoId) {
throw new Error("ID de vidéo introuvable dans la réponse YouTube");
}
const youtubeLink = `https://www.youtube.com/watch?v=${videoId}`;
await projectsRef.doc(projectId).set(
{
youtubeStatus: "PUBLISHED",
youtubePublished: true,
youtubeUrl: youtubeLink,
youtubeVideoId: videoId,
youtubePublishedAt: admin.firestore.FieldValue.serverTimestamp(),
youtubeError: null,
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
},
{ merge: true }
);
logger.info("[publishPlaybackToYoutube] publication réussie", {
projectId,
videoId,
});
return {
videoId,
youtubeUrl: youtubeLink,
};
} catch (error) {
logger.error("[publishPlaybackToYoutube] échec de publication", {
projectId,
error: error?.message,
});
const errorMessage =
error instanceof HttpsError
? error.message
: error?.message || "Publication YouTube échouée";
await projectsRef.doc(projectId).set(
{
youtubeStatus: "FAILED",
youtubePublished: false,
youtubeError: errorMessage,
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
},
{ merge: true }
);
if (error instanceof HttpsError) {
throw error;
}
throw new HttpsError("internal", errorMessage);
} finally {
await fsp.rm(tmpDir, { recursive: true, force: true });
}
}
);