initial commit
This commit is contained in:
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
|
||||
|
||||
htmlexample
|
||||
results
|
||||
47
Taskfile.yml
Normal file
47
Taskfile.yml
Normal file
@@ -0,0 +1,47 @@
|
||||
version: "3"
|
||||
|
||||
tasks:
|
||||
build:
|
||||
desc: Build TypeScript files in src/ to dist/ using Bun
|
||||
cmds:
|
||||
- bun build ./src/popup/popup.ts --outdir ./dist/popup
|
||||
- bun build ./src/background.ts --outdir ./dist
|
||||
- bun build ./src/contentScript.ts --outdir ./dist
|
||||
|
||||
- cp ./src/popup/popup.html ./dist/popup/popup.html
|
||||
- cp ./src/popup/popup.css ./dist/popup/popup.css
|
||||
- cp ./src/public/manifest.json ./dist/manifest.json
|
||||
- cp ./src/mark.css ./dist/mark.css
|
||||
- zip -r ./public/extension.zip ./dist
|
||||
|
||||
dev:
|
||||
desc: Watch mode development
|
||||
cmds:
|
||||
- task: clean
|
||||
- task: build
|
||||
- |
|
||||
bun --watch build --outdir ./dist/popup ./src/popup/popup.ts &
|
||||
bun --watch build --outdir ./dist ./src/background.ts &
|
||||
bun --watch build --outdir ./dist ./src/contentScript.ts &
|
||||
while inotifywait -e modify,create,delete -r ./src/popup/popup.html ./src/popup/popup.css ./src/public/manifest.json .src/; do
|
||||
cp ./src/popup/popup.html ./dist/popup/popup.html
|
||||
cp ./src/popup/popup.css ./dist/popup/popup.css
|
||||
cp ./src/mark.css ./dist/mark.css
|
||||
cp ./src/public/manifest.json ./dist/manifest.json
|
||||
done
|
||||
wait
|
||||
|
||||
clean:
|
||||
desc: Remove dist directory
|
||||
cmds:
|
||||
- rm -rf dist
|
||||
|
||||
lint:
|
||||
desc: Run TypeScript compiler for type checking
|
||||
cmds:
|
||||
- tsc --noEmit
|
||||
|
||||
start:
|
||||
desc: Alias for dev
|
||||
cmds:
|
||||
- task: dev
|
||||
288
bun.lock
Normal file
288
bun.lock
Normal file
@@ -0,0 +1,288 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "new-browser-input",
|
||||
"dependencies": {
|
||||
"@types/turndown": "^5.0.5",
|
||||
"turndown": "^7.2.0",
|
||||
"turndown-plugin-gfm": "^1.0.2",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"@types/chrome": "^0.0.326",
|
||||
"@typescript-eslint/eslint-plugin": "^8.34.0",
|
||||
"@typescript-eslint/parser": "^8.34.0",
|
||||
"eslint": "^9.28.0",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.8.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="],
|
||||
|
||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
|
||||
|
||||
"@eslint/config-array": ["@eslint/config-array@0.20.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ=="],
|
||||
|
||||
"@eslint/config-helpers": ["@eslint/config-helpers@0.2.2", "", {}, "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg=="],
|
||||
|
||||
"@eslint/core": ["@eslint/core@0.14.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg=="],
|
||||
|
||||
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
|
||||
|
||||
"@eslint/js": ["@eslint/js@9.28.0", "", {}, "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg=="],
|
||||
|
||||
"@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="],
|
||||
|
||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.1", "", { "dependencies": { "@eslint/core": "^0.14.0", "levn": "^0.4.1" } }, "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w=="],
|
||||
|
||||
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||
|
||||
"@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
|
||||
|
||||
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
|
||||
|
||||
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
||||
|
||||
"@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="],
|
||||
|
||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||
|
||||
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
||||
|
||||
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.15", "", { "dependencies": { "bun-types": "1.2.15" } }, "sha512-U1ljPdBEphF0nw1MIk0hI7kPg7dFdPyM7EenHsp6W5loNHl7zqy6JQf/RKCgnUn2KDzUpkBwHPnEJEjII594bA=="],
|
||||
|
||||
"@types/chrome": ["@types/chrome@0.0.326", "", { "dependencies": { "@types/filesystem": "*", "@types/har-format": "*" } }, "sha512-WS7jKf3ZRZFHOX7dATCZwqNJgdfiSF0qBRFxaO0LhIOvTNBrfkab26bsZwp6EBpYtqp8loMHJTnD6vDTLWPKYw=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/filesystem": ["@types/filesystem@0.0.36", "", { "dependencies": { "@types/filewriter": "*" } }, "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA=="],
|
||||
|
||||
"@types/filewriter": ["@types/filewriter@0.0.33", "", {}, "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g=="],
|
||||
|
||||
"@types/har-format": ["@types/har-format@1.2.16", "", {}, "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/node": ["@types/node@24.0.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-yZQa2zm87aRVcqDyH5+4Hv9KYgSdgwX1rFnGvpbzMaC7YAljmhBET93TPiTd3ObwTL+gSpIzPKg5BqVxdCvxKg=="],
|
||||
|
||||
"@types/turndown": ["@types/turndown@5.0.5", "", {}, "sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.34.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.34.0", "@typescript-eslint/type-utils": "8.34.0", "@typescript-eslint/utils": "8.34.0", "@typescript-eslint/visitor-keys": "8.34.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.34.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w=="],
|
||||
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.34.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.34.0", "@typescript-eslint/types": "8.34.0", "@typescript-eslint/typescript-estree": "8.34.0", "@typescript-eslint/visitor-keys": "8.34.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA=="],
|
||||
|
||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.34.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.34.0", "@typescript-eslint/types": "^8.34.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw=="],
|
||||
|
||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.34.0", "", { "dependencies": { "@typescript-eslint/types": "8.34.0", "@typescript-eslint/visitor-keys": "8.34.0" } }, "sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw=="],
|
||||
|
||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.34.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA=="],
|
||||
|
||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.34.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.34.0", "@typescript-eslint/utils": "8.34.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-n7zSmOcUVhcRYC75W2pnPpbO1iwhJY3NLoHEtbJwJSNlVAZuwqu05zY3f3s2SDWWDSo9FdN5szqc73DCtDObAg=="],
|
||||
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.34.0", "", {}, "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.34.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.34.0", "@typescript-eslint/tsconfig-utils": "8.34.0", "@typescript-eslint/types": "8.34.0", "@typescript-eslint/visitor-keys": "8.34.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg=="],
|
||||
|
||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.34.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.34.0", "@typescript-eslint/types": "8.34.0", "@typescript-eslint/typescript-estree": "8.34.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.34.0", "", { "dependencies": { "@typescript-eslint/types": "8.34.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA=="],
|
||||
|
||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
|
||||
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.15", "", { "dependencies": { "@types/node": "*" } }, "sha512-NarRIaS+iOaQU1JPfyKhZm4AsUOrwUOqRNHY0XxI8GI8jYxiLXLcdjYMG9UKS+fwWasc1uw1htV9AX24dD+p4w=="],
|
||||
|
||||
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||
|
||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
|
||||
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"eslint": ["eslint@9.28.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.1", "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.28.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ=="],
|
||||
|
||||
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
||||
|
||||
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
|
||||
|
||||
"espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
|
||||
|
||||
"esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
|
||||
|
||||
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
|
||||
|
||||
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||
|
||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||
|
||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||
|
||||
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||
|
||||
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
|
||||
|
||||
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
||||
|
||||
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||
|
||||
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
||||
|
||||
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
||||
|
||||
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
|
||||
|
||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
|
||||
"globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
||||
|
||||
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
|
||||
"ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||
|
||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||
|
||||
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
|
||||
|
||||
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||
|
||||
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
||||
|
||||
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||
|
||||
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||
|
||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||
|
||||
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
||||
|
||||
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||
|
||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||
|
||||
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
||||
|
||||
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
||||
|
||||
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||
|
||||
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
||||
|
||||
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||
|
||||
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||
|
||||
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||
|
||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||
|
||||
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||
|
||||
"ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
|
||||
|
||||
"turndown": ["turndown@7.2.0", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A=="],
|
||||
|
||||
"turndown-plugin-gfm": ["turndown-plugin-gfm@1.0.2", "", {}, "sha512-vwz9tfvF7XN/jE0dGoBei3FXWuvll78ohzCZQuOb+ZjWrs3a0XhQVomJEb2Qh4VHTPNRO4GPZh0V7VRbiWwkRg=="],
|
||||
|
||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
|
||||
}
|
||||
}
|
||||
766
example-json/instagram-post.json
Normal file
766
example-json/instagram-post.json
Normal file
@@ -0,0 +1,766 @@
|
||||
[
|
||||
{
|
||||
"facts": [
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.instagram.com/p/DMVZrA6hg4s/"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "jokowi"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Terima kasih Bapak Presiden Prabowo yang telah berkenan berkunjung kerumah keluarga kami di Solo"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "480461"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "13247"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/",
|
||||
"name": "createdAt",
|
||||
"type": "datetime",
|
||||
"value": "1753024829"
|
||||
}
|
||||
],
|
||||
"savedAt": "2025-07-22T13:00:16.478Z",
|
||||
"type": "post",
|
||||
"url": "https://www.instagram.com/p/DMVZrA6hg4s/"
|
||||
},
|
||||
{
|
||||
"facts": [
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=jeus.simorangkir",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "jeus.simorangkir"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=jeus.simorangkir",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "TOMBOL HIDUP PAK JOKOWI DAN SEHAT SELALU🔥🔥🔥"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=jeus.simorangkir",
|
||||
"name": "profileUrl",
|
||||
"type": "url",
|
||||
"value": "https://www.instagram.com/jeus.simorangkir/"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=jeus.simorangkir",
|
||||
"name": "profileImage",
|
||||
"type": "url",
|
||||
"value": "https://instagram.fdps17-1.fna.fbcdn.net/v/t51.2885-19/322979687_211880454702196_544204852800779256_n.jpg?stp=dst-jpg_s150x150_tt6&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby44MjguYzIifQ&_nc_ht=instagram.fdps17-1.fna.fbcdn.net&_nc_cat=104&_nc_oc=Q6cZ2QEaTl0keKoiKKc6csP2iVA4GuXZLcz8QtSg9_FYgwP0G4r2rVJmXH2pJfYTzpTRXeI&_nc_ohc=GqDz8hC__CIQ7kNvwF9-w44&_nc_gid=_-U-WBOzzjboJzKoYa-Reg&edm=APs17CUBAAAA&ccb=7-5&oh=00_AfT_j_J85vGg7Dh5NbYez0nQ6jgaKggFeqrTGvnqOUpgdA&oe=68854C2E&_nc_sid=10d13b"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=jeus.simorangkir",
|
||||
"name": "timestamp",
|
||||
"type": "datetime",
|
||||
"value": "2025-07-20T15:32:08.000Z"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=jeus.simorangkir",
|
||||
"name": "timestampDisplay",
|
||||
"type": "string",
|
||||
"value": "1d"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=jeus.simorangkir",
|
||||
"name": "likes",
|
||||
"type": "number",
|
||||
"value": "7457"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=jeus.simorangkir",
|
||||
"name": "replies",
|
||||
"type": "number",
|
||||
"value": "86"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=angga.sudibyo",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "angga.sudibyo"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=angga.sudibyo",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Presiden terbaik dan mantan presiden terbaik, sehat2 selalu pak Jokowi dan pak Prabowo..❤️"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=angga.sudibyo",
|
||||
"name": "profileUrl",
|
||||
"type": "url",
|
||||
"value": "https://www.instagram.com/angga.sudibyo/"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=angga.sudibyo",
|
||||
"name": "profileImage",
|
||||
"type": "url",
|
||||
"value": "https://instagram.fdps17-1.fna.fbcdn.net/v/t51.2885-19/130992335_2899596323615403_6005254264143156705_n.jpg?stp=dst-jpg_s150x150_tt6&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby43MjAuYzIifQ&_nc_ht=instagram.fdps17-1.fna.fbcdn.net&_nc_cat=103&_nc_oc=Q6cZ2QEaTl0keKoiKKc6csP2iVA4GuXZLcz8QtSg9_FYgwP0G4r2rVJmXH2pJfYTzpTRXeI&_nc_ohc=98ypeD2jh0UQ7kNvwG3K6qe&_nc_gid=_-U-WBOzzjboJzKoYa-Reg&edm=APs17CUBAAAA&ccb=7-5&oh=00_AfSWbG3p05bu31rxEAX4E7rrj4sG3EvPzDIBmqx8gBeGfg&oe=68857447&_nc_sid=10d13b"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=angga.sudibyo",
|
||||
"name": "timestamp",
|
||||
"type": "datetime",
|
||||
"value": "2025-07-20T15:21:50.000Z"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=angga.sudibyo",
|
||||
"name": "timestampDisplay",
|
||||
"type": "string",
|
||||
"value": "1d"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=angga.sudibyo",
|
||||
"name": "likes",
|
||||
"type": "number",
|
||||
"value": "6374"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=angga.sudibyo",
|
||||
"name": "replies",
|
||||
"type": "number",
|
||||
"value": "138"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=hubbimuhammad",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "hubbimuhammad"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=hubbimuhammad",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Wiwok detok"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=hubbimuhammad",
|
||||
"name": "profileUrl",
|
||||
"type": "url",
|
||||
"value": "https://www.instagram.com/hubbimuhammad/"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=hubbimuhammad",
|
||||
"name": "profileImage",
|
||||
"type": "url",
|
||||
"value": "https://instagram.fdps17-1.fna.fbcdn.net/v/t51.2885-19/236438774_1003962147037394_5773809719213231100_n.jpg?stp=dst-jpg_s150x150_tt6&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby4xMDgwLmMyIn0&_nc_ht=instagram.fdps17-1.fna.fbcdn.net&_nc_cat=102&_nc_oc=Q6cZ2QEaTl0keKoiKKc6csP2iVA4GuXZLcz8QtSg9_FYgwP0G4r2rVJmXH2pJfYTzpTRXeI&_nc_ohc=Uj3-r0VSatoQ7kNvwFmHm57&_nc_gid=_-U-WBOzzjboJzKoYa-Reg&edm=APs17CUBAAAA&ccb=7-5&oh=00_AfTe1e2-KCVDwT9pylBSDKZPk5Cbblq8XHWSovIYuRTB3Q&oe=688554AB&_nc_sid=10d13b"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=hubbimuhammad",
|
||||
"name": "timestamp",
|
||||
"type": "datetime",
|
||||
"value": "2025-07-20T15:46:10.000Z"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=hubbimuhammad",
|
||||
"name": "timestampDisplay",
|
||||
"type": "string",
|
||||
"value": "1d"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=hubbimuhammad",
|
||||
"name": "likes",
|
||||
"type": "number",
|
||||
"value": "201"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=hubbimuhammad",
|
||||
"name": "replies",
|
||||
"type": "number",
|
||||
"value": "6"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=dr_amar___Verified",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "dr_amar___Verified"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=dr_amar___Verified",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "dr_amar___"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=dr_amar___Verified",
|
||||
"name": "profileUrl",
|
||||
"type": "url",
|
||||
"value": "https://www.instagram.com/dr_amar___/"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=dr_amar___Verified",
|
||||
"name": "profileImage",
|
||||
"type": "url",
|
||||
"value": "https://instagram.fdps17-1.fna.fbcdn.net/v/t51.2885-19/371372306_352964293817124_2624783820092888996_n.jpg?stp=dst-jpg_s150x150_tt6&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby4xMDc5LmMyIn0&_nc_ht=instagram.fdps17-1.fna.fbcdn.net&_nc_cat=100&_nc_oc=Q6cZ2QEaTl0keKoiKKc6csP2iVA4GuXZLcz8QtSg9_FYgwP0G4r2rVJmXH2pJfYTzpTRXeI&_nc_ohc=E8WNw887aPEQ7kNvwFFJyCm&_nc_gid=_-U-WBOzzjboJzKoYa-Reg&edm=APs17CUBAAAA&ccb=7-5&oh=00_AfSfxrjfovS9WBPQon5Z3LX6n-3FwCzM-lxuKY-keNE60Q&oe=68856DAF&_nc_sid=10d13b"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=dr_amar___Verified",
|
||||
"name": "timestamp",
|
||||
"type": "datetime",
|
||||
"value": "2025-07-20T15:30:13.000Z"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=dr_amar___Verified",
|
||||
"name": "timestampDisplay",
|
||||
"type": "string",
|
||||
"value": "1d"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=dr_amar___Verified",
|
||||
"name": "likes",
|
||||
"type": "number",
|
||||
"value": "3850"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=dr_amar___Verified",
|
||||
"name": "replies",
|
||||
"type": "number",
|
||||
"value": "27"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=0_stanly_0",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "0_stanly_0"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=0_stanly_0",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Indah nya kebersamaan para pemimpin bangsa"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=0_stanly_0",
|
||||
"name": "profileUrl",
|
||||
"type": "url",
|
||||
"value": "https://www.instagram.com/0_stanly_0/"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=0_stanly_0",
|
||||
"name": "profileImage",
|
||||
"type": "url",
|
||||
"value": "https://instagram.fdps17-1.fna.fbcdn.net/v/t51.2885-19/321843554_1273204200194583_2783570366240423599_n.jpg?stp=dst-jpg_s150x150_tt6&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby4xMDgwLmMyIn0&_nc_ht=instagram.fdps17-1.fna.fbcdn.net&_nc_cat=103&_nc_oc=Q6cZ2QEaTl0keKoiKKc6csP2iVA4GuXZLcz8QtSg9_FYgwP0G4r2rVJmXH2pJfYTzpTRXeI&_nc_ohc=oBdLq4jqMKYQ7kNvwGT8ws8&_nc_gid=_-U-WBOzzjboJzKoYa-Reg&edm=APs17CUBAAAA&ccb=7-5&oh=00_AfRRh7dEs0eJuwaJx_wLnzxTsSg3Qo988djd9ehPS_SLqQ&oe=68854E46&_nc_sid=10d13b"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=0_stanly_0",
|
||||
"name": "timestamp",
|
||||
"type": "datetime",
|
||||
"value": "2025-07-20T15:32:17.000Z"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=0_stanly_0",
|
||||
"name": "timestampDisplay",
|
||||
"type": "string",
|
||||
"value": "1d"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=0_stanly_0",
|
||||
"name": "likes",
|
||||
"type": "number",
|
||||
"value": "1587"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=0_stanly_0",
|
||||
"name": "replies",
|
||||
"type": "number",
|
||||
"value": "17"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=riansya_14",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "riansya_14"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=riansya_14",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Full penjilat😂"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=riansya_14",
|
||||
"name": "profileUrl",
|
||||
"type": "url",
|
||||
"value": "https://www.instagram.com/riansya_14/"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=riansya_14",
|
||||
"name": "profileImage",
|
||||
"type": "url",
|
||||
"value": "https://instagram.fdps17-1.fna.fbcdn.net/v/t51.2885-19/481788343_1696217614321706_2181028944150197706_n.jpg?stp=dst-jpg_s150x150_tt6&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby4xMDgwLmMyIn0&_nc_ht=instagram.fdps17-1.fna.fbcdn.net&_nc_cat=102&_nc_oc=Q6cZ2QEaTl0keKoiKKc6csP2iVA4GuXZLcz8QtSg9_FYgwP0G4r2rVJmXH2pJfYTzpTRXeI&_nc_ohc=a10J0WzO0pwQ7kNvwFBpxu6&_nc_gid=_-U-WBOzzjboJzKoYa-Reg&edm=APs17CUBAAAA&ccb=7-5&oh=00_AfTYsWzU58ByhjeDbSCnu_r_9PgcKiBuAmoDV4-ne6qalQ&oe=688568FB&_nc_sid=10d13b"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=riansya_14",
|
||||
"name": "timestamp",
|
||||
"type": "datetime",
|
||||
"value": "2025-07-20T22:36:53.000Z"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=riansya_14",
|
||||
"name": "timestampDisplay",
|
||||
"type": "string",
|
||||
"value": "1d"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=riansya_14",
|
||||
"name": "likes",
|
||||
"type": "number",
|
||||
"value": "251"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=riansya_14",
|
||||
"name": "replies",
|
||||
"type": "number",
|
||||
"value": "8"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=cipris_1303",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "cipris_1303"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=cipris_1303",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Presiden terbaik dan mantan presiden terbaik 🇮🇩sehat selalu pak dan penuh berkat"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=cipris_1303",
|
||||
"name": "profileUrl",
|
||||
"type": "url",
|
||||
"value": "https://www.instagram.com/cipris_1303/"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=cipris_1303",
|
||||
"name": "profileImage",
|
||||
"type": "url",
|
||||
"value": "https://instagram.fdps17-1.fna.fbcdn.net/v/t51.2885-19/142858735_422560428973422_381148379720372387_n.jpg?stp=dst-jpg_s150x150_tt6&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby4xMDgwLmMyIn0&_nc_ht=instagram.fdps17-1.fna.fbcdn.net&_nc_cat=106&_nc_oc=Q6cZ2QEaTl0keKoiKKc6csP2iVA4GuXZLcz8QtSg9_FYgwP0G4r2rVJmXH2pJfYTzpTRXeI&_nc_ohc=s86wIuGVFJgQ7kNvwG0sf68&_nc_gid=_-U-WBOzzjboJzKoYa-Reg&edm=APs17CUBAAAA&ccb=7-5&oh=00_AfTe6WX77ADhOfPa3zr6FuhRt4QuvoSLXK01Wxcj4edIvA&oe=6885429A&_nc_sid=10d13b"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=cipris_1303",
|
||||
"name": "timestamp",
|
||||
"type": "datetime",
|
||||
"value": "2025-07-20T23:28:05.000Z"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=cipris_1303",
|
||||
"name": "timestampDisplay",
|
||||
"type": "string",
|
||||
"value": "1d"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=cipris_1303",
|
||||
"name": "likes",
|
||||
"type": "number",
|
||||
"value": "1506"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=cipris_1303",
|
||||
"name": "replies",
|
||||
"type": "number",
|
||||
"value": "19"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=prayogaza1",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "prayogaza1"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=prayogaza1",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Merusak Tatanan negara"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=prayogaza1",
|
||||
"name": "profileUrl",
|
||||
"type": "url",
|
||||
"value": "https://www.instagram.com/prayogaza1/"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=prayogaza1",
|
||||
"name": "profileImage",
|
||||
"type": "url",
|
||||
"value": "https://instagram.fdps17-1.fna.fbcdn.net/v/t51.2885-19/515761503_18081358546871883_7627397364361180093_n.jpg?stp=dst-jpg_s150x150_tt6&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby4xMDgwLmMyIn0&_nc_ht=instagram.fdps17-1.fna.fbcdn.net&_nc_cat=103&_nc_oc=Q6cZ2QEaTl0keKoiKKc6csP2iVA4GuXZLcz8QtSg9_FYgwP0G4r2rVJmXH2pJfYTzpTRXeI&_nc_ohc=irohdz2KZJEQ7kNvwEZpRIF&_nc_gid=_-U-WBOzzjboJzKoYa-Reg&edm=APs17CUBAAAA&ccb=7-5&oh=00_AfTm_Qsry34RyzquINRic-v-bSIuKf8VJMEM2qhfagVWzw&oe=688546BC&_nc_sid=10d13b"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=prayogaza1",
|
||||
"name": "timestamp",
|
||||
"type": "datetime",
|
||||
"value": "2025-07-21T00:56:42.000Z"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=prayogaza1",
|
||||
"name": "timestampDisplay",
|
||||
"type": "string",
|
||||
"value": "1d"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=prayogaza1",
|
||||
"name": "likes",
|
||||
"type": "number",
|
||||
"value": "565"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=prayogaza1",
|
||||
"name": "replies",
|
||||
"type": "number",
|
||||
"value": "14"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=togarsitumorangofficialVerified",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "togarsitumorangofficialVerified"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=togarsitumorangofficialVerified",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "togarsitumorangofficial"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=togarsitumorangofficialVerified",
|
||||
"name": "profileUrl",
|
||||
"type": "url",
|
||||
"value": "https://www.instagram.com/togarsitumorangofficial/"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=togarsitumorangofficialVerified",
|
||||
"name": "profileImage",
|
||||
"type": "url",
|
||||
"value": "https://instagram.fdps17-1.fna.fbcdn.net/v/t51.2885-19/402739086_1059202125232608_3579411792619616185_n.jpg?stp=dst-jpg_s150x150_tt6&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby4xMDgwLmMyIn0&_nc_ht=instagram.fdps17-1.fna.fbcdn.net&_nc_cat=100&_nc_oc=Q6cZ2QEaTl0keKoiKKc6csP2iVA4GuXZLcz8QtSg9_FYgwP0G4r2rVJmXH2pJfYTzpTRXeI&_nc_ohc=HYiQ5qjvW4EQ7kNvwHRMkUe&_nc_gid=_-U-WBOzzjboJzKoYa-Reg&edm=APs17CUBAAAA&ccb=7-5&oh=00_AfT5oO3RDah0Srg61xAT-LB5p3mndqL_ti1Iv4vsEFyV7A&oe=6885559F&_nc_sid=10d13b"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=togarsitumorangofficialVerified",
|
||||
"name": "timestamp",
|
||||
"type": "datetime",
|
||||
"value": "2025-07-20T23:11:43.000Z"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=togarsitumorangofficialVerified",
|
||||
"name": "timestampDisplay",
|
||||
"type": "string",
|
||||
"value": "1d"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=togarsitumorangofficialVerified",
|
||||
"name": "likes",
|
||||
"type": "number",
|
||||
"value": "2959"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=togarsitumorangofficialVerified",
|
||||
"name": "replies",
|
||||
"type": "number",
|
||||
"value": "86"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=28_rynt",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "28_rynt"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=28_rynt",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "banyak buzzer nya ya ternyata 😂"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=28_rynt",
|
||||
"name": "profileUrl",
|
||||
"type": "url",
|
||||
"value": "https://www.instagram.com/28_rynt/"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=28_rynt",
|
||||
"name": "profileImage",
|
||||
"type": "url",
|
||||
"value": "https://instagram.fdps17-1.fna.fbcdn.net/v/t51.2885-19/518501742_17939081997031581_6677039797516417206_n.jpg?stp=dst-jpg_s150x150_tt6&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby4xMDgwLmMyIn0&_nc_ht=instagram.fdps17-1.fna.fbcdn.net&_nc_cat=109&_nc_oc=Q6cZ2QEaTl0keKoiKKc6csP2iVA4GuXZLcz8QtSg9_FYgwP0G4r2rVJmXH2pJfYTzpTRXeI&_nc_ohc=wSTQ-uSfplkQ7kNvwH1I5Wg&_nc_gid=_-U-WBOzzjboJzKoYa-Reg&edm=APs17CUBAAAA&ccb=7-5&oh=00_AfS1lyPhB-WlI5qjJ8tn1KgqjpjG7ObK7Ws9HGtvF4nUlw&oe=68856E37&_nc_sid=10d13b"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=28_rynt",
|
||||
"name": "timestamp",
|
||||
"type": "datetime",
|
||||
"value": "2025-07-20T15:37:45.000Z"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=28_rynt",
|
||||
"name": "timestampDisplay",
|
||||
"type": "string",
|
||||
"value": "1d"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=28_rynt",
|
||||
"name": "likes",
|
||||
"type": "number",
|
||||
"value": "729"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=28_rynt",
|
||||
"name": "replies",
|
||||
"type": "number",
|
||||
"value": "26"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=luxvillle_",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "luxvillle_"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=luxvillle_",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "ijazah mana wi"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=luxvillle_",
|
||||
"name": "profileUrl",
|
||||
"type": "url",
|
||||
"value": "https://www.instagram.com/luxvillle_/"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=luxvillle_",
|
||||
"name": "profileImage",
|
||||
"type": "url",
|
||||
"value": "https://instagram.fcpq1-1.fna.fbcdn.net/v/t51.2885-19/464760996_1254146839119862_3605321457742435801_n.png?stp=dst-jpg_e0_s150x150_tt6&cb=8577c754-c2464923&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby4xNTAuYzIifQ&_nc_ht=instagram.fcpq1-1.fna.fbcdn.net&_nc_cat=1&_nc_oc=Q6cZ2QEaG6Jc7yy7TBh3OXus9ZPqiTE7msO3-ppRfRbIu3XYolX60KH8ILHIlfhHK3GQ1OnFv5A20Yos2h-2EiD8Pa6B&_nc_ohc=wwy9HdRJcYoQ7kNvwFOLNAK&_nc_gid=WjEoo0wXG3yPxgmIBxxJCw&edm=AB11_MABAAAA&ccb=7-5&ig_cache_key=YW5vbnltb3VzX3Byb2ZpbGVfcGlj.3-ccb7-5-cb8577c754-c2464923&oh=00_AfQbu6nENHFskdPLtIGGTEEjmuDgY-MTOzx28TFmSGks7w&oe=68855F28&_nc_sid=dc5e7f"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=luxvillle_",
|
||||
"name": "timestamp",
|
||||
"type": "datetime",
|
||||
"value": "2025-07-20T15:55:10.000Z"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=luxvillle_",
|
||||
"name": "timestampDisplay",
|
||||
"type": "string",
|
||||
"value": "1d"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=luxvillle_",
|
||||
"name": "likes",
|
||||
"type": "number",
|
||||
"value": "141"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=luxvillle_",
|
||||
"name": "replies",
|
||||
"type": "number",
|
||||
"value": "4"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=srikandi_sarangheyo",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "srikandi_sarangheyo"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=srikandi_sarangheyo",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "bpk jokowi ttp dihati rakyatnya,,masyaallah sht sll bpk smoga allah mlindungi bpk bserta kluarga,,bpk adlh president terbaik yg prnh bangsa indonesia miliki🔥🔥"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=srikandi_sarangheyo",
|
||||
"name": "profileUrl",
|
||||
"type": "url",
|
||||
"value": "https://www.instagram.com/srikandi_sarangheyo/"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=srikandi_sarangheyo",
|
||||
"name": "profileImage",
|
||||
"type": "url",
|
||||
"value": "https://instagram.fdps17-1.fna.fbcdn.net/v/t51.2885-19/511638400_18008389931784271_6744633162976478269_n.jpg?stp=dst-jpg_s150x150_tt6&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby4xMDgwLmMyIn0&_nc_ht=instagram.fdps17-1.fna.fbcdn.net&_nc_cat=107&_nc_oc=Q6cZ2QEaTl0keKoiKKc6csP2iVA4GuXZLcz8QtSg9_FYgwP0G4r2rVJmXH2pJfYTzpTRXeI&_nc_ohc=uv_u2hKQs0EQ7kNvwEdYLR7&_nc_gid=_-U-WBOzzjboJzKoYa-Reg&edm=APs17CUBAAAA&ccb=7-5&oh=00_AfS8u2iUSeHLBq-BGzAAnvIawjWLOR_epP0b-TCYAEyUCw&oe=6885504A&_nc_sid=10d13b"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=srikandi_sarangheyo",
|
||||
"name": "timestamp",
|
||||
"type": "datetime",
|
||||
"value": "2025-07-20T15:25:59.000Z"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=srikandi_sarangheyo",
|
||||
"name": "timestampDisplay",
|
||||
"type": "string",
|
||||
"value": "1d"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=srikandi_sarangheyo",
|
||||
"name": "likes",
|
||||
"type": "number",
|
||||
"value": "1589"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=srikandi_sarangheyo",
|
||||
"name": "replies",
|
||||
"type": "number",
|
||||
"value": "34"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=aufa_suryanata",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "aufa_suryanata"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=aufa_suryanata",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Anjay banyak bner buzer 58🔥"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=aufa_suryanata",
|
||||
"name": "profileUrl",
|
||||
"type": "url",
|
||||
"value": "https://www.instagram.com/aufa_suryanata/"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=aufa_suryanata",
|
||||
"name": "profileImage",
|
||||
"type": "url",
|
||||
"value": "https://instagram.fcpq1-1.fna.fbcdn.net/v/t51.2885-19/464760996_1254146839119862_3605321457742435801_n.png?stp=dst-jpg_e0_s150x150_tt6&cb=8577c754-c2464923&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby4xNTAuYzIifQ&_nc_ht=instagram.fcpq1-1.fna.fbcdn.net&_nc_cat=1&_nc_oc=Q6cZ2QEaG6Jc7yy7TBh3OXus9ZPqiTE7msO3-ppRfRbIu3XYolX60KH8ILHIlfhHK3GQ1OnFv5A20Yos2h-2EiD8Pa6B&_nc_ohc=wwy9HdRJcYoQ7kNvwFOLNAK&_nc_gid=WjEoo0wXG3yPxgmIBxxJCw&edm=AB11_MABAAAA&ccb=7-5&ig_cache_key=YW5vbnltb3VzX3Byb2ZpbGVfcGlj.3-ccb7-5-cb8577c754-c2464923&oh=00_AfQbu6nENHFskdPLtIGGTEEjmuDgY-MTOzx28TFmSGks7w&oe=68855F28&_nc_sid=dc5e7f"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=aufa_suryanata",
|
||||
"name": "timestamp",
|
||||
"type": "datetime",
|
||||
"value": "2025-07-22T07:19:31.000Z"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=aufa_suryanata",
|
||||
"name": "timestampDisplay",
|
||||
"type": "string",
|
||||
"value": "5h"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=aufa_suryanata",
|
||||
"name": "likes",
|
||||
"type": "number",
|
||||
"value": "20"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=mhmmdalfarhzy",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "mhmmdalfarhzy"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=mhmmdalfarhzy",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Neraka pedih loh pak"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=mhmmdalfarhzy",
|
||||
"name": "profileUrl",
|
||||
"type": "url",
|
||||
"value": "https://www.instagram.com/mhmmdalfarhzy/"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=mhmmdalfarhzy",
|
||||
"name": "profileImage",
|
||||
"type": "url",
|
||||
"value": "https://instagram.fdps17-1.fna.fbcdn.net/v/t51.2885-19/469247047_2288541594840403_8660919058584834110_n.jpg?stp=dst-jpg_s150x150_tt6&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby4xMDgwLmMyIn0&_nc_ht=instagram.fdps17-1.fna.fbcdn.net&_nc_cat=107&_nc_oc=Q6cZ2QEaTl0keKoiKKc6csP2iVA4GuXZLcz8QtSg9_FYgwP0G4r2rVJmXH2pJfYTzpTRXeI&_nc_ohc=hLDSRNi7BUIQ7kNvwFRdWyf&_nc_gid=_-U-WBOzzjboJzKoYa-Reg&edm=APs17CUBAAAA&ccb=7-5&oh=00_AfTOj2NHnotkdZC8GdpMEiCOrqXIL9H7XfmKnXkfdALcxQ&oe=688561D1&_nc_sid=10d13b"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=mhmmdalfarhzy",
|
||||
"name": "timestamp",
|
||||
"type": "datetime",
|
||||
"value": "2025-07-21T05:49:38.000Z"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=mhmmdalfarhzy",
|
||||
"name": "timestampDisplay",
|
||||
"type": "string",
|
||||
"value": "1d"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=mhmmdalfarhzy",
|
||||
"name": "likes",
|
||||
"type": "number",
|
||||
"value": "93"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=mhmmdalfarhzy",
|
||||
"name": "replies",
|
||||
"type": "number",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=nicke_cubby",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "nicke_cubby"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=nicke_cubby",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Ma Sya Allah 💕 adem liat 2 idola rakyat Indonesia ini 😍❤️"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=nicke_cubby",
|
||||
"name": "profileUrl",
|
||||
"type": "url",
|
||||
"value": "https://www.instagram.com/nicke_cubby/"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=nicke_cubby",
|
||||
"name": "profileImage",
|
||||
"type": "url",
|
||||
"value": "https://instagram.fdps17-1.fna.fbcdn.net/v/t51.2885-19/109421841_2712419008993610_1068649358446491562_n.jpg?stp=dst-jpg_s150x150_tt6&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby4xMDgwLmMyIn0&_nc_ht=instagram.fdps17-1.fna.fbcdn.net&_nc_cat=102&_nc_oc=Q6cZ2QEaTl0keKoiKKc6csP2iVA4GuXZLcz8QtSg9_FYgwP0G4r2rVJmXH2pJfYTzpTRXeI&_nc_ohc=wvTdnd3HqdcQ7kNvwHkdBNY&_nc_gid=_-U-WBOzzjboJzKoYa-Reg&edm=APs17CUBAAAA&ccb=7-5&oh=00_AfRmc1NYxyktG2CjJar9oaULcg6tYkN7QcWF1lKcEsOgUw&oe=68855D4B&_nc_sid=10d13b"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=nicke_cubby",
|
||||
"name": "timestamp",
|
||||
"type": "datetime",
|
||||
"value": "2025-07-20T15:33:03.000Z"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=nicke_cubby",
|
||||
"name": "timestampDisplay",
|
||||
"type": "string",
|
||||
"value": "1d"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=nicke_cubby",
|
||||
"name": "likes",
|
||||
"type": "number",
|
||||
"value": "1138"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/p/DMVZrA6hg4s/#comment_by=nicke_cubby",
|
||||
"name": "replies",
|
||||
"type": "number",
|
||||
"value": "19"
|
||||
}
|
||||
],
|
||||
"savedAt": "2025-07-22T13:00:16.481Z",
|
||||
"type": "comment",
|
||||
"url": "https://www.instagram.com/p/DMVZrA6hg4s/"
|
||||
}
|
||||
]
|
||||
262
example-json/instagram-profile.json
Normal file
262
example-json/instagram-profile.json
Normal file
@@ -0,0 +1,262 @@
|
||||
[
|
||||
{
|
||||
"facts": [
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "jokowi"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/",
|
||||
"name": "profileImage",
|
||||
"type": "url",
|
||||
"value": "https://instagram.fdps17-1.fna.fbcdn.net/v/t51.2885-19/323796872_186095804235435_4099239256456033831_n.jpg?stp=dst-jpg_s150x150_tt6&efg=eyJ2ZW5jb2RlX3RhZyI6InByb2ZpbGVfcGljLmRqYW5nby4xMDgwLmMyIn0&_nc_ht=instagram.fdps17-1.fna.fbcdn.net&_nc_cat=1&_nc_oc=Q6cZ2QFFdz0HZSILtfF6Rzwy-7QTXpo6wKoqXnj-acYERdRgEiC2b09IA6YcYhLsoIcwtnM&_nc_ohc=FBLkTWMPdcEQ7kNvwEsWlAd&_nc_gid=wq3phUaaZcxz05d9N8p1CQ&edm=AP4sbd4BAAAA&ccb=7-5&oh=00_AfQLAy2aidNZT4Wwk2yjo-vzoQnYGy3TGkcncOMrfS0Fig&oe=68856664&_nc_sid=7a9f4b"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/",
|
||||
"name": "isVerified",
|
||||
"type": "boolean",
|
||||
"value": "true"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/",
|
||||
"name": "postCount",
|
||||
"type": "number",
|
||||
"value": "5775"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/",
|
||||
"name": "followingCount",
|
||||
"type": "number",
|
||||
"value": "0"
|
||||
}
|
||||
],
|
||||
"savedAt": "2025-07-22T12:58:32.657Z",
|
||||
"type": "profile",
|
||||
"url": "https://www.instagram.com/jokowi/"
|
||||
},
|
||||
{
|
||||
"facts": [
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/reel/DMYCVUvB9nM/",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.instagram.com/jokowi/reel/DMYCVUvB9nM/"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/reel/DMYCVUvB9nM/",
|
||||
"name": "thumbnailUrl",
|
||||
"type": "url",
|
||||
"value": "https://scontent.cdninstagram.com/v/t51.71878-15/521411526_1921944571903368_794586515625105760_n.jpg?stp=dst-jpg_e15_tt6&_nc_cat=1&ig_cache_key=MzY4MTcwMjk1Njg0MTI5NDI4NA%3D%3D.3-ccb1-7&ccb=1-7&_nc_sid=58cdad&efg=eyJ2ZW5jb2RlX3RhZyI6InhwaWRzLjY0MHgxMTM2LnNkciJ9&_nc_ohc=2zdzHEyojxUQ7kNvwEPv5y-&_nc_oc=AdkldIrtVzgWwYEOaLiluo1rvyS_A2-B693SDTzUAO3OYgrQU5x1xPjBZG_R3ImK3eE&_nc_ad=z-m&_nc_cid=0&_nc_zt=23&_nc_ht=scontent.cdninstagram.com&_nc_gid=wq3phUaaZcxz05d9N8p1CQ&oh=00_AfRIXv_U69iPTc24p6k-cAozoWWtH2OkttS6sBK8AdyvUw&oe=688559A1"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/reel/DMYCVUvB9nM/",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Saya melepas kepulangan Bapak Presiden Prabowo di Bandara Adi Soemarmo. Semoga lancar dan aman sampai tujuan."
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/reel/DMXGi5vhtc0/",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.instagram.com/jokowi/reel/DMXGi5vhtc0/"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/reel/DMXGi5vhtc0/",
|
||||
"name": "thumbnailUrl",
|
||||
"type": "url",
|
||||
"value": "https://scontent.cdninstagram.com/v/t51.71878-15/522690099_2768611616656149_5103259914330463227_n.jpg?stp=dst-jpg_e15_tt6&_nc_cat=1&ig_cache_key=MzY4MTQ0MDAwNzE0MDU5NTUwOA%3D%3D.3-ccb1-7&ccb=1-7&_nc_sid=58cdad&efg=eyJ2ZW5jb2RlX3RhZyI6InhwaWRzLjY0MHgxMTM2LnNkciJ9&_nc_ohc=C7PTKqmsmd0Q7kNvwE3rKzd&_nc_oc=AdnzWqP91dz_O-galyznJsFEEVa-gde1YhxXUIjyv0ksUGdWV1WE-2C3fUZT9kHESEA&_nc_ad=z-m&_nc_cid=0&_nc_zt=23&_nc_ht=scontent.cdninstagram.com&_nc_gid=wq3phUaaZcxz05d9N8p1CQ&oh=00_AfT-kRhFgWph3lL0bQl7yD9bIjADvynfJcWHZLN0JP7PGw&oe=6885662A"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/reel/DMXGi5vhtc0/",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Saya mengajak Bapak Presiden Prabowo menikmati hidangan Bakmi Bu Citro di Solo. Rumah makan sederhana yang punya rasa istimewa. Kami duduk santai, berbincang ringan ditemani semangkuk bakmi hangat. Sebuah momen personal, jauh dari agenda kenegaraan, dan berkesan. Terima kasih Pak Presiden"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/reel/DMVZrA6hg4s/",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.instagram.com/jokowi/reel/DMVZrA6hg4s/"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/reel/DMVZrA6hg4s/",
|
||||
"name": "thumbnailUrl",
|
||||
"type": "url",
|
||||
"value": "https://scontent.cdninstagram.com/v/t51.71878-15/521542555_1681705549156478_6214360678065370356_n.jpg?stp=dst-jpg_e15_tt6&_nc_cat=1&ig_cache_key=MzY4MDk2MTE3NzUyNzM4OTc0MA%3D%3D.3-ccb1-7&ccb=1-7&_nc_sid=58cdad&efg=eyJ2ZW5jb2RlX3RhZyI6InhwaWRzLjY0MHgxMTM2LnNkciJ9&_nc_ohc=cZRzNEaAtoMQ7kNvwEwQnCJ&_nc_oc=AdngYOxEGuzX9uDXG93nZ4gJHlfFPstMyFFdtBQ6InHETh-psATyJflNlMaPqxh2cfI&_nc_ad=z-m&_nc_cid=0&_nc_zt=23&_nc_ht=scontent.cdninstagram.com&_nc_gid=wq3phUaaZcxz05d9N8p1CQ&oh=00_AfRXFZaASm9kMqbECDOXllWQryvG0UEe3_OIrsahtVIlNg&oe=68855335"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/reel/DMVZrA6hg4s/",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Terima kasih Bapak Presiden Prabowo yang telah berkenan berkunjung kerumah keluarga kami di Solo"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/reel/DMFubsQhxmM/",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.instagram.com/jokowi/reel/DMFubsQhxmM/"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/reel/DMFubsQhxmM/",
|
||||
"name": "thumbnailUrl",
|
||||
"type": "url",
|
||||
"value": "https://scontent.cdninstagram.com/v/t51.71878-15/519411790_1237579021145938_1676132418911526372_n.jpg?stp=dst-jpg_e15_tt6&_nc_cat=100&ig_cache_key=MzY3NjU0ODg4MzkwNTE5MDI4NA%3D%3D.3-ccb1-7&ccb=1-7&_nc_sid=58cdad&efg=eyJ2ZW5jb2RlX3RhZyI6InhwaWRzLjY0MHgxMTM2LnNkciJ9&_nc_ohc=mjXPCkkF1BEQ7kNvwFelyVV&_nc_oc=AdkgQSZbSinyAtN9uVIVIFoVwYjJK53PQ_lG-e-JjKvuiYpi26wTgh5XSbI1AItKglA&_nc_ad=z-m&_nc_cid=0&_nc_zt=23&_nc_ht=scontent.cdninstagram.com&_nc_gid=wq3phUaaZcxz05d9N8p1CQ&oh=00_AfSKXEpujxufjLtS3TD1IzmRvIoB6wy6bq0JH3v3TiPmwQ&oe=6885762A"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/reel/DMFubsQhxmM/",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Pertunjukan spesial dari mahasiswa-mahasiswi UKI Yogyakarta di depan kediaman. Terima kasih atas perhatian dan dukungannya yang hangat!"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/reel/DLumay5Bx0t/",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.instagram.com/jokowi/reel/DLumay5Bx0t/"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/reel/DLumay5Bx0t/",
|
||||
"name": "thumbnailUrl",
|
||||
"type": "url",
|
||||
"value": "https://scontent.cdninstagram.com/v/t51.71878-15/516449709_712998244821804_8121518606345055304_n.jpg?stp=dst-jpg_e15_tt6&_nc_cat=110&ig_cache_key=MzY3MDAzOTcxMzQ3MTIwODc0OQ%3D%3D.3-ccb1-7&ccb=1-7&_nc_sid=58cdad&efg=eyJ2ZW5jb2RlX3RhZyI6InhwaWRzLjY0MHgxMTM2LnNkciJ9&_nc_ohc=BUh_kYhiXRkQ7kNvwGT773l&_nc_oc=AdmtFFurX4j0wddMlHXHQBL3o1LjYNen-NEX4IW10pFiQwqvSo503GSRLiea0UXXGvE&_nc_ad=z-m&_nc_cid=0&_nc_zt=23&_nc_ht=scontent.cdninstagram.com&_nc_gid=wq3phUaaZcxz05d9N8p1CQ&oh=00_AfQLiieHKzgC3DTqATTd57PSootEGOdsz655J1KEZhymoQ&oe=6885746E"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/reel/DLumay5Bx0t/",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Pagi ini saya mencoba naik ATV di pantai bersama cucu-cucu. Ternyata sulit sekali untuk berhenti 😄"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/p/DLooTsthS_m/",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.instagram.com/jokowi/p/DLooTsthS_m/"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/p/DLooTsthS_m/",
|
||||
"name": "thumbnailUrl",
|
||||
"type": "url",
|
||||
"value": "https://instagram.fdps17-1.fna.fbcdn.net/v/t51.2885-15/514848573_18487163449075048_432826180196670794_n.webp?efg=eyJ2ZW5jb2RlX3RhZyI6IkNBUk9VU0VMX0lURU0uaW1hZ2VfdXJsZ2VuLjE0NDB4MTkyMC5zZHIuZjgyNzg3LmRlZmF1bHRfaW1hZ2UuYzIifQ&_nc_ht=instagram.fdps17-1.fna.fbcdn.net&_nc_cat=104&_nc_oc=Q6cZ2QFFdz0HZSILtfF6Rzwy-7QTXpo6wKoqXnj-acYERdRgEiC2b09IA6YcYhLsoIcwtnM&_nc_ohc=tc2BcnYHFecQ7kNvwEgTAT8&_nc_gid=wq3phUaaZcxz05d9N8p1CQ&edm=AP4sbd4BAAAA&ccb=7-5&ig_cache_key=MzY2ODM1OTE1OTY0MjE0NTAzNw%3D%3D.3-ccb7-5&oh=00_AfSLMXnOqzAwvenVnnk6b9pbQ4NVMMC3EfSZoP415asR6w&oe=688558A2&_nc_sid=7a9f4b"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/p/DLooTsthS_m/",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Senang dapat bermain bersama cucu-cucu di pantai saat liburan sekolah. Momen seperti ini sederhana, tapi menyegarkan pikiran dan menenangkan hati."
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/reel/DLbyOP8hJu2/",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.instagram.com/jokowi/reel/DLbyOP8hJu2/"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/reel/DLbyOP8hJu2/",
|
||||
"name": "thumbnailUrl",
|
||||
"type": "url",
|
||||
"value": "https://scontent.cdninstagram.com/v/t51.71878-15/510435322_3173564999475228_4451451499021688820_n.jpg?stp=dst-jpg_e15_tt6&_nc_cat=111&ig_cache_key=MzY2NDc0MzYwMzMxNTcxMDkwMg%3D%3D.3-ccb1-7&ccb=1-7&_nc_sid=58cdad&efg=eyJ2ZW5jb2RlX3RhZyI6InhwaWRzLjY0MHgxMTM2LnNkciJ9&_nc_ohc=AkQwuEW_EDkQ7kNvwEvFDYy&_nc_oc=AdnMGkD2AfN02pLszomu44YXNAEJRsmBoBpYFPw6zCkjA6zRavvMKon9eqjVDRm8LuY&_nc_ad=z-m&_nc_cid=0&_nc_zt=23&_nc_ht=scontent.cdninstagram.com&_nc_gid=wq3phUaaZcxz05d9N8p1CQ&oh=00_AfSslKc6ESnaZ0WgbyG71KTu8cuAyGcKu2hxD-bSKywLbQ&oe=688575D0"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/reel/DLbyOP8hJu2/",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Pak Harsudi dari Sukabumi menunjukkan bahwa kepedulian pada lingkungan bisa diwujudkan dengan cara yang sederhana namun penuh makna. Selama hampir lima tahun, beliau mengumpulkan limbah plastik dan merajutnya menjadi karpet daur ulang sepanjang 11 meter. Bukan hanya karya seni, tapi juga simbol ketekunan dan kreativitas.\n\nKarya ini mengingatkan kita bahwa setiap tindakan kecil bisa membawa dampak besar. Dengan niat yang tulus dan proses yang konsisten, bahkan limbah pun bisa menjadi sesuatu yang bernilai dan menginspirasi banyak orang. Terima kasih atas semangat dan dedikasinya, Pak Harsudi."
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/reel/DLY6SUmh5c5/",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.instagram.com/jokowi/reel/DLY6SUmh5c5/"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/reel/DLY6SUmh5c5/",
|
||||
"name": "thumbnailUrl",
|
||||
"type": "url",
|
||||
"value": "https://scontent.cdninstagram.com/v/t51.71878-15/510435059_1012640944365927_3735622432573815901_n.jpg?stp=dst-jpg_e15_tt6&_nc_cat=111&ig_cache_key=MzY2MzkzNDY0MjYzNTM4MDUzNw%3D%3D.3-ccb1-7&ccb=1-7&_nc_sid=58cdad&efg=eyJ2ZW5jb2RlX3RhZyI6InhwaWRzLjY0MHgxMTM2LnNkciJ9&_nc_ohc=13LX2_NgLBcQ7kNvwGLI9VY&_nc_oc=Adl4-EAsojhjH0WGHAd_qhYVCG7sAFkXgnR41qqFTsCIelQTduNlITFoC3seo3quhYM&_nc_ad=z-m&_nc_cid=0&_nc_zt=23&_nc_ht=scontent.cdninstagram.com&_nc_gid=wq3phUaaZcxz05d9N8p1CQ&oh=00_AfQJIR9uSqyHls1ci1nCXQqHU5sPM9R_u31OHnCI6H6A-A&oe=68854E0C"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/reel/DLY6SUmh5c5/",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Selamat Tahun Baru Islam 1447 Hijriyah.\nMomen ini mengingatkan kita bahwa perubahan sejati datang dari ketulusan dan kebersamaan.\nMari terus jaga semangat kerukunan dan perdamaian."
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/reel/DLOTHJxBinr/",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.instagram.com/jokowi/reel/DLOTHJxBinr/"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/reel/DLOTHJxBinr/",
|
||||
"name": "thumbnailUrl",
|
||||
"type": "url",
|
||||
"value": "https://scontent.cdninstagram.com/v/t51.71878-15/509890521_1078717800840222_2399100765814600721_n.jpg?stp=dst-jpg_e15_tt6&_nc_cat=111&ig_cache_key=MzY2MDk0NzYwMTUwNTAwNDAxMQ%3D%3D.3-ccb1-7&ccb=1-7&_nc_sid=58cdad&efg=eyJ2ZW5jb2RlX3RhZyI6InhwaWRzLjY0MHgxMTM2LnNkciJ9&_nc_ohc=dvxpahQ3jxkQ7kNvwG0eACM&_nc_oc=Adk55rv_61TRv88p9UeFgmUy6wfLCNY4EwSNlUsz5H8PwYJ8OpaEoPhYvu8NorJvZCw&_nc_ad=z-m&_nc_cid=0&_nc_zt=23&_nc_ht=scontent.cdninstagram.com&_nc_gid=wq3phUaaZcxz05d9N8p1CQ&oh=00_AfSxRdG_yej7D-r4xhMMU7_DJnUO_wtPdzQj2pPB_GQJ2Q&oe=688559D5"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/reel/DLOTHJxBinr/",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Terima kasih untuk setiap doa, ucapan, dan karangan bunga.\nSaya dan keluarga merasa sangat tersentuh dan dikuatkan oleh perhatian dan kasih sayang yang diberikan oleh Bapak Presiden @prabowo, para sahabat dan masyarakat dari berbagai penjuru negeri.\n\nSemoga semua kebaikan yang disampaikan dibalas berlipat ganda oleh Tuhan Yang Maha Esa. 🙏"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/reel/DKjzwc1hu3h/",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.instagram.com/jokowi/reel/DKjzwc1hu3h/"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/reel/DKjzwc1hu3h/",
|
||||
"name": "thumbnailUrl",
|
||||
"type": "url",
|
||||
"value": "https://scontent.cdninstagram.com/v/t51.71878-15/503869592_1344177693307964_4256342620284667217_n.jpg?stp=dst-jpg_e15_tt6&_nc_cat=103&ig_cache_key=MzY0ODk4Nzc1Mjk2OTk4OTYwMQ%3D%3D.3-ccb1-7&ccb=1-7&_nc_sid=58cdad&efg=eyJ2ZW5jb2RlX3RhZyI6InhwaWRzLjY0MHgxMTM2LnNkciJ9&_nc_ohc=768D6mzr0VcQ7kNvwHr2gaW&_nc_oc=AdnIO2SkIRPGLosYEkkqzBK7eJoCYNXoMLSW1DYc75tjZvOr2eM6T88jDgincWRtyz4&_nc_ad=z-m&_nc_cid=0&_nc_zt=23&_nc_ht=scontent.cdninstagram.com&_nc_gid=wq3phUaaZcxz05d9N8p1CQ&oh=00_AfSc5A5I8n59F7xNrK7bpn3unnzMG_TYG_3xRsBwDV5quA&oe=688576B1"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/reel/DKjzwc1hu3h/",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Jum'at pagi tadi saya melaksanakan Salat Iduladha di halaman Gedung Graha Saba, Solo, kemudian bersilaturahmi dan beramah tamah bersama warga sekitar.\n\nSelamat merayakan Iduladha bagi seluruh Umat Islam, di mana pun berada."
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/reel/DKimymYh3R0/",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.instagram.com/jokowi/reel/DKimymYh3R0/"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/reel/DKimymYh3R0/",
|
||||
"name": "thumbnailUrl",
|
||||
"type": "url",
|
||||
"value": "https://scontent.cdninstagram.com/v/t51.71878-15/504107158_1249135363550582_4731561455237896905_n.jpg?stp=dst-jpg_e15_tt6&_nc_cat=100&ig_cache_key=MzY0ODY0OTI1MTA3ODUwMTQ5Mg%3D%3D.3-ccb1-7&ccb=1-7&_nc_sid=58cdad&efg=eyJ2ZW5jb2RlX3RhZyI6InhwaWRzLjY0MHgxMTM2LnNkciJ9&_nc_ohc=u7LQpfNHPKsQ7kNvwGR0CaP&_nc_oc=AdmQ65GSIGuY_rQTVVwajV9vpe25A5BTfLY0y1FAE99b9z6nsaplQkQnCi3i9xMLrzY&_nc_ad=z-m&_nc_cid=0&_nc_zt=23&_nc_ht=scontent.cdninstagram.com&_nc_gid=wq3phUaaZcxz05d9N8p1CQ&oh=00_AfQXIlc1LIdrgRZZuj5hca5U4xSMEe0gmQXPvJFLQwMH1w&oe=688554A3"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/reel/DKimymYh3R0/",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Semoga semangat berkurban menumbuhkan keikhlasan, kepedulian terhadap sesama, dan keteguhan iman dalam diri kita. Mohon maaf lahir dan batin.\n\nSelamat Hari Raya Iduladha 1446 Hijriah. Semoga Allah SWT senantiasa memberkahi langkah kita semua."
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/reel/DKiV5v1B_XH/",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.instagram.com/jokowi/reel/DKiV5v1B_XH/"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/reel/DKiV5v1B_XH/",
|
||||
"name": "thumbnailUrl",
|
||||
"type": "url",
|
||||
"value": "https://scontent.cdninstagram.com/v/t51.75761-15/504271573_18482683051075048_3073227615232722894_n.jpg?stp=dst-jpg_e15_tt6&_nc_cat=104&ig_cache_key=MzY0ODU3NDk3NTQ2NjAxMDA1NTE4NDgyNjgzMDQ4MDc1MDQ4.3-ccb1-7&ccb=1-7&_nc_sid=58cdad&efg=eyJ2ZW5jb2RlX3RhZyI6InhwaWRzLjU0MHg5NjAuc2RyIn0%3D&_nc_ohc=uGujMGeVkbUQ7kNvwFolwtb&_nc_oc=Admpr0cVy-TQAu2oBchjp9MVZIPeJNggP6XfeP9pf7xlY-A5CtbGily_of6xkJEJgYo&_nc_ad=z-m&_nc_cid=0&_nc_zt=23&_nc_ht=scontent.cdninstagram.com&_nc_gid=wq3phUaaZcxz05d9N8p1CQ&oh=00_AfTNxK_mkS4aF_9fkKEDmhElDQptpl0xk3A5-T3P1JK74A&oe=68857230"
|
||||
},
|
||||
{
|
||||
"context": "https://www.instagram.com/jokowi/reel/DKiV5v1B_XH/",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Kita bersyukur dan bangga. Terima kasih kepada seluruh pemain dan tim pelatih Timnas Indonesia atas semangat juang dan kerja keras yang luar biasa. Kemenangan atas China bukan hanya hasil di atas lapangan, tetapi juga menjadi bukti bahwa Indonesia berhasil menempati peringkat keempat klasemen grup dan melaju ke putaran keempat kualifikasi Piala Dunia. Selamat untuk Timnas Garuda. Kalian telah mengharumkan nama bangsa."
|
||||
}
|
||||
],
|
||||
"savedAt": "2025-07-22T12:58:32.659Z",
|
||||
"type": "post",
|
||||
"url": "https://www.instagram.com/jokowi/"
|
||||
}
|
||||
]
|
||||
142
example-json/linkedin-post.json
Normal file
142
example-json/linkedin-post.json
Normal file
@@ -0,0 +1,142 @@
|
||||
[
|
||||
{
|
||||
"facts": [
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216",
|
||||
"name": "url",
|
||||
"type": "string",
|
||||
"value": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "Quincy Larson"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "The freeCodeCamp community is publishing SO MANY books and courses. We're building a life-long learner's paradise. Here are this week's 5 resources worth your time: \n \n1\\. freeCodeCamp just published this comprehensive guide to AI app security and the most common vulnerabilities. You'll learn about Threat Modeling, Prompt Injection, Data Poisoning, supply chain risks, and more. (1 hour YouTube course): [https://lnkd.in/gzEnPDnt](https://lnkd.in/gzEnPDnt) \n \n2\\. JavaScript is the most popular language on the planet. But it's also super duper prone to errors. Luckily, freeCodeCamp just dropped this comprehensive handbook to help you understand how JavaScript's error handling works. You'll learn about Try-Catch, Error Rethrowing, the Finally keyword, and the Error Object itself. We also show tons of example code that you can scrutinize and learn from. (full-length handbook): [https://lnkd.in/gm8tpKu9](https://lnkd.in/gm8tpKu9) \n \n3\\. On this week's podcast, I interview a developer who had to apply to 800 jobs, but eventually landed one. Braydon Coyer started out building mobile apps in high school. At one point his iPhone game even out-sold Angry Birds for a day or two. He shares tons of strategies for applying for developer roles, sane ways to integrate AI into your developer workflows, and how to switch from mobile app dev to full stack dev. This dude is a blast. (1 hour watch or listen in your favorite podcast app): [https://lnkd.in/gGjSbXMw](https://lnkd.in/gGjSbXMw) \n \n4\\. I'm a huge fan of data visualization, and I love me some D3.js. So I was jazzed about this new course that'll help you shore up your Data Viz fundamentals. You'll go from bare-bones scatter plots to dynamically updating charts with fancy animations. If you want to learn how to make your data more accessible and more fun, this course is for you. (90 minute YouTube course): [https://lnkd.in/g6UnAK6M](https://lnkd.in/g6UnAK6M) \n \n5\\. As you may know, freeCodeCamp is a big open source project. And we have tons of developers who jump in to help us improve our curriculum and our codebase. But it's common for many of them to lose steam and drop off the map. This guide will help ensure that this doesn't happen to you. It'll give you actionable tips for setting open source goals, finding the right projects to get involved in, engaging with fellow devs, and more. (15 minute read): [https://lnkd.in/gr-\\_Wexr](https://lnkd.in/gr-_Wexr) \n \nQuote of the Week: \n“When I was learning to code, the big picture didn’t click until I needed to solve real problems. That’s when variables and arrays started making sense — not in theory, but in practice.” — Software Engineer Braydon Coyer on this week's freeCodeCamp podcast \n \nIf you read this far... support our charity's mission: [https://lnkd.in/gSGSfE8b](https://lnkd.in/gSGSfE8b) 🏕️\n\nLove this, Quincy. Much needed for me.\n\nThanks for sharing, Quincy\n\nThanks for sharing, Quincy\n\n"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "233"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "3"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216",
|
||||
"name": "shareCount",
|
||||
"type": "number",
|
||||
"value": "20"
|
||||
}
|
||||
],
|
||||
"savedAt": "2025-07-22T12:54:19.113Z",
|
||||
"type": "post",
|
||||
"url": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/"
|
||||
},
|
||||
{
|
||||
"facts": [
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/#comment_by=rex-kishore",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "Kishore Kumar"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/#comment_by=rex-kishore",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Love this, Quincy. Much needed for me."
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/#comment_by=rex-kishore",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/#comment_by=rex-kishore",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/#comment_by=rex-kishore",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "3hari"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/#comment_by=oluwafemi-adebayo-606688159",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "Oluwafemi Adebayo"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/#comment_by=oluwafemi-adebayo-606688159",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Thanks for sharing, Quincy"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/#comment_by=oluwafemi-adebayo-606688159",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/#comment_by=oluwafemi-adebayo-606688159",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/#comment_by=oluwafemi-adebayo-606688159",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "1hari"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/#comment_by=daretechie",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "Dare A."
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/#comment_by=daretechie",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Thanks for sharing, Quincy"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/#comment_by=daretechie",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/#comment_by=daretechie",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/#comment_by=daretechie",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "3hari"
|
||||
}
|
||||
],
|
||||
"savedAt": "2025-07-22T12:54:19.136Z",
|
||||
"type": "comment",
|
||||
"url": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216/"
|
||||
}
|
||||
]
|
||||
220
example-json/linkedin-profile.json
Normal file
220
example-json/linkedin-profile.json
Normal file
@@ -0,0 +1,220 @@
|
||||
[
|
||||
{
|
||||
"facts": [
|
||||
{
|
||||
"context": "https://www.linkedin.com/in/quincylarson",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "Quincy Larson"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/in/quincylarson",
|
||||
"name": "description",
|
||||
"type": "string",
|
||||
"value": "Teacher and founder of freeCodeCamp.org 🏕️ Host of the freeCodeCamp Podcast 🎧"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/in/quincylarson",
|
||||
"name": "profileImage",
|
||||
"type": "url",
|
||||
"value": "https://media.licdn.com/dms/image/v2/C5603AQFET2zJ01O5Ug/profile-displayphoto-shrink_100_100/profile-displayphoto-shrink_100_100/0/1617221558171?e=1756339200&v=beta&t=rhCIDfIiPMN-COe0-niFes8YhUdBXBrulQHxCT7pxf4"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/in/quincylarson",
|
||||
"name": "followerCount",
|
||||
"type": "number",
|
||||
"value": "4109"
|
||||
}
|
||||
],
|
||||
"savedAt": "2025-07-22T12:53:23.051Z",
|
||||
"type": "profile",
|
||||
"url": "https://www.linkedin.com/in/quincylarson/recent-activity/all/"
|
||||
},
|
||||
{
|
||||
"facts": [
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7353277001924837376",
|
||||
"name": "url",
|
||||
"type": "string",
|
||||
"value": "https://www.linkedin.com/feed/update/urn:li:activity:7353277001924837376"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7353277001924837376",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "Quincy Larson"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7353277001924837376",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "The Tokyo subway system UX is on another level. Check out this handy chart of which train car you should choose for the fastest exit from each destination station.\n\n"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7353277001924837376",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "30"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7353277001924837376",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7353277001924837376",
|
||||
"name": "shareCount",
|
||||
"type": "number",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216",
|
||||
"name": "url",
|
||||
"type": "string",
|
||||
"value": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "Quincy Larson"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "The freeCodeCamp community is publishing SO MANY books and courses. We're building a life-long learner's paradise. Here are this week's 5 resources worth your time: \n \n1\\. freeCodeCamp just published this comprehensive guide to AI app security and the most common vulnerabilities. You'll learn about Threat Modeling, Prompt Injection, Data Poisoning, supply chain risks, and more. (1 hour YouTube course): [https://lnkd.in/gzEnPDnt](https://lnkd.in/gzEnPDnt) \n \n2\\. JavaScript is the most popular language on the planet. But it's also super duper prone to errors. Luckily, freeCodeCamp just dropped this comprehensive handbook to help you understand how JavaScript's error handling works. You'll learn about Try-Catch, Error Rethrowing, the Finally keyword, and the Error Object itself. We also show tons of example code that you can scrutinize and learn from. (full-length handbook): [https://lnkd.in/gm8tpKu9](https://lnkd.in/gm8tpKu9) \n \n3\\. On this week's podcast, I interview a developer who had to apply to 800 jobs, but eventually landed one. Braydon Coyer started out building mobile apps in high school. At one point his iPhone game even out-sold Angry Birds for a day or two. He shares tons of strategies for applying for developer roles, sane ways to integrate AI into your developer workflows, and how to switch from mobile app dev to full stack dev. This dude is a blast. (1 hour watch or listen in your favorite podcast app): [https://lnkd.in/gGjSbXMw](https://lnkd.in/gGjSbXMw) \n \n4\\. I'm a huge fan of data visualization, and I love me some D3.js. So I was jazzed about this new course that'll help you shore up your Data Viz fundamentals. You'll go from bare-bones scatter plots to dynamically updating charts with fancy animations. If you want to learn how to make your data more accessible and more fun, this course is for you. (90 minute YouTube course): [https://lnkd.in/g6UnAK6M](https://lnkd.in/g6UnAK6M) \n \n5\\. As you may know, freeCodeCamp is a big open source project. And we have tons of developers who jump in to help us improve our curriculum and our codebase. But it's common for many of them to lose steam and drop off the map. This guide will help ensure that this doesn't happen to you. It'll give you actionable tips for setting open source goals, finding the right projects to get involved in, engaging with fellow devs, and more. (15 minute read): [https://lnkd.in/gr-\\_Wexr](https://lnkd.in/gr-_Wexr) \n \nQuote of the Week: \n“When I was learning to code, the big picture didn’t click until I needed to solve real problems. That’s when variables and arrays started making sense — not in theory, but in practice.” — Software Engineer Braydon Coyer on this week's freeCodeCamp podcast \n \nIf you read this far... support our charity's mission: [https://lnkd.in/gSGSfE8b](https://lnkd.in/gSGSfE8b) 🏕️\n\n"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "233"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "3"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7351977300806025216",
|
||||
"name": "shareCount",
|
||||
"type": "number",
|
||||
"value": "20"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7350887231995199490",
|
||||
"name": "url",
|
||||
"type": "string",
|
||||
"value": "https://www.linkedin.com/feed/update/urn:li:activity:7350887231995199490"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7350887231995199490",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "Quincy Larson"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7350887231995199490",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "I just gave my first-ever presentation completely in Japanese to the Keio Computer Society. 日本語勉強大好き🍣 If you’re a native Japanese speaker near Shinjuku and want to language exchange DM me. I’m ~N3 level and want to practice more dev-focused terms. 会話しよう。☕️"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7350887231995199490",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "347"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7350887231995199490",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "38"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7350887231995199490",
|
||||
"name": "shareCount",
|
||||
"type": "number",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7349465038635614209",
|
||||
"name": "url",
|
||||
"type": "string",
|
||||
"value": "https://www.linkedin.com/feed/update/urn:li:activity:7349465038635614209"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7349465038635614209",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "Quincy Larson"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7349465038635614209",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "I just sent out this week's \"5 freeCodeCamp resources worth your time.\" Some heaters in here including a 2-hour course on agentic \"vibe coding\" with n8n. \n \n1\\. freeCodeCamp just published this comprehensive course that will teach you how to build apps using the LAMP Stack – Linux, Apache, MySQL, PHP. This is a classic toolchain that a huge number of websites still use to this day. You can code along at home and build your own clone of Google Calendar – complete with multi-appointment support and booking conflict logic. Then you'll use JavaScript to add a dynamic user interface. This project will help you expand your horizons and get in some serious reps. (3 hour YouTube course): [https://lnkd.in/gw56n5dr](https://lnkd.in/gw56n5dr) \n \n2\\. You may have heard the term “vibe coding”, where you write the specifications for your app, then hand it over to code generation agents who write the code for you. This approach has been a bit polarizing in the developer community, with vocal critics and vocal advocates. My advice is don't listen to any of them. Think for yourself. freeCodeCamp just published this course that will introduce you to vibe coding tools like n8n – an open source workflow automation tool – so you can wire together APIs and services without needing to write a ton of code manually. (2 hour YouTube course): [https://lnkd.in/gzM6-9ws](https://lnkd.in/gzM6-9ws) \n \n3\\. On this week's podcast I interview Joe Hill. He's a software engineer who works on a data platform for NASA. Joe taught himself programming for 4 years while working as a janitor. As the single father of two Autistic boys, he first used his programming skills to build an iPad app to help them learn how to talk. He shares tons of practical tips for learning new skills and for getting things done inside big orgs. (1 hour watch or listen in your favorite podcast app): [https://lnkd.in/gsvEUU-E](https://lnkd.in/gsvEUU-E) \n \n4\\. Learn how to write Python tests so you can make your codebase more robust. This tutorial will teach you the basics of pytest and show you tons of examples. You'll learn about Markers, Fixtures, Parameterization, and more. (20 minute read): [https://lnkd.in/gafgJ8nq](https://lnkd.in/gafgJ8nq) \n \n5\\. Tell your gardener friends: freeCodeCamp just published a tutorial on how to monitor the moisture of your soil using an Arduino microcontroller. This tutorial will show you what hardware to get and how to wire it up. Then it'll walk you through the code you'll run, and explain how everything works. (30 minute read): [https://lnkd.in/gggjuHhU](https://lnkd.in/gggjuHhU) \n \nQuote of the Week: \n“Becoming tool agnostic is one of the biggest strengths you will ever have as a programmer. If you walk into a place and they say “we use XYZ”, you can go “Great. I'll learn it.” That is so powerful.” - Joe Hill on this week's podcast\n\n"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7349465038635614209",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "193"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7349465038635614209",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "7"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7349465038635614209",
|
||||
"name": "shareCount",
|
||||
"type": "number",
|
||||
"value": "9"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7348694351415341056",
|
||||
"name": "url",
|
||||
"type": "string",
|
||||
"value": "https://www.linkedin.com/feed/update/urn:li:activity:7348694351415341056"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7348694351415341056",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "Nielda Karla Melo"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7348694351415341056",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Quando comecei a escrever esse post me veio um grande sorriso no rosto. Isso porque eu tô aqui para avisar que o episódio desse mês do [freeCodeCamp](https://www.linkedin.com/company/free-code-camp/) em português é a minha entrevista com o [](/in/teocalvo/)[Téo Calvo](https://www.linkedin.com/in/ACoAAA3X1sABWVV0Ru1ijaPUvBgcXVc7xsZR6Eo). \nEu me diverti tanto nessa conversa! Espero que vocês curtam também. \n \nVem assistir na integra aqui: [https://lnkd.in/dAsgBXVP](https://lnkd.in/dAsgBXVP) \n \nTéo, obrigada pela troca e espero em breve poder conversar de novo! 😃\n\n"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7348694351415341056",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "59"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7348694351415341056",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "4"
|
||||
},
|
||||
{
|
||||
"context": "https://www.linkedin.com/feed/update/urn:li:activity:7348694351415341056",
|
||||
"name": "shareCount",
|
||||
"type": "number",
|
||||
"value": "1"
|
||||
}
|
||||
],
|
||||
"savedAt": "2025-07-22T12:53:23.052Z",
|
||||
"type": "post",
|
||||
"url": "https://www.linkedin.com/in/quincylarson/recent-activity/all/"
|
||||
}
|
||||
]
|
||||
508
example-json/tiktok-post.json
Normal file
508
example-json/tiktok-post.json
Normal file
@@ -0,0 +1,508 @@
|
||||
[
|
||||
{
|
||||
"facts": [
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "Metro TV"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Kelangkaan air bersih di Gaza, Palestina sebabkan warga harus hidup 4-5 hari tanpa air karena bergantung pada pengiriman truk air. \n \n[**#tiktokmetrotv**](/tag/tiktokmetrotv) [**#tiktokberita**](/tag/tiktokberita) [**#beritaterkini**](/tag/beritaterkini) [**#metrotv**](/tag/metrotv) [**#viral**](/tag/viral) [**#fyp**](/tag/fyp) [**#gaza**](/tag/gaza) [**#air**](/tag/air) [**#airbersih**](/tag/airbersih) [**#warga**](/tag/warga) [**#palestine**](/tag/palestine)"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "576"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "16"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "20m ago"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529858656179932436"
|
||||
}
|
||||
],
|
||||
"savedAt": "2025-07-22T12:50:59.763Z",
|
||||
"type": "post",
|
||||
"url": "https://www.tiktok.com/@metro_tv/video/7529858656179932436"
|
||||
},
|
||||
{
|
||||
"facts": [
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=aabbaa1207",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "aba•"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=aabbaa1207",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "aabbaa1207"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=aabbaa1207",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Free Palestine 🇵🇸🇵🇸🇵🇸"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=aabbaa1207",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "3"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=aabbaa1207",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "15m ago"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=aditaputra59",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "putra A nya alicia😋🤭"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=aditaputra59",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "aditaputra59"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=aditaputra59",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "pada sewot cepet² an komen jir😹🤭"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=aditaputra59",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "2"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=aditaputra59",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "16m ago"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=billieeilishgxxx",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "S"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=billieeilishgxxx",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "billieeilishgxxx"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=billieeilishgxxx",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "sementara itu di tanah Papua tidak ada yang perduli"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=billieeilishgxxx",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=billieeilishgxxx",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "8m ago"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=surayaismail06",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "suraya"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=surayaismail06",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "surayaismail06"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=surayaismail06",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "sabar2 Allah bersama kalian saudaraku😭😭😭😭😭"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=surayaismail06",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=surayaismail06",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "14m ago"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=korizen_sastra45",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "korizen_sastra"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=korizen_sastra45",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "korizen_sastra45"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=korizen_sastra45",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "pertama"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=korizen_sastra45",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=korizen_sastra45",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "20m ago"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=user421553031236",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "reza"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=user421553031236",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "user421553031236"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=user421553031236",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "astagfirullah"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=user421553031236",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=user421553031236",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "9m ago"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=latifh4785",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "latifh"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=latifh4785",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "latifh4785"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=latifh4785",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "free palestina"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=latifh4785",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=latifh4785",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "11m ago"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=nabila_azzahra700",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "Nabila Azzahra Nst"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=nabila_azzahra700",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "nabila_azzahra700"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=nabila_azzahra700",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "FREE PALESTINE\nSAVE PALESTINE"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=nabila_azzahra700",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=nabila_azzahra700",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "4m ago"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=geren096",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "GREN.FR"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=geren096",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "geren096"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=geren096",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "ke 4"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=geren096",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=geren096",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "18m ago"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=korizen_sastra45",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "korizen_sastra"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=korizen_sastra45",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "korizen_sastra45"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=korizen_sastra45",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "yess pertama"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=korizen_sastra45",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=korizen_sastra45",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "20m ago"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=1nuranisa",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "hamba allah"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=1nuranisa",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "1nuranisa"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=1nuranisa",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "free palestine"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=1nuranisa",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=1nuranisa",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "13m ago"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=korizen_sastra45",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "korizen_sastra"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=korizen_sastra45",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "korizen_sastra45"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=korizen_sastra45",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "sebelum ribuan"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=korizen_sastra45",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=korizen_sastra45",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "20m ago"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=abang_ojol30",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "Herman _MAXIM OJEK"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=abang_ojol30",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "abang_ojol30"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=abang_ojol30",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "free Israel"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=abang_ojol30",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=abang_ojol30",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "4m ago"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=the_cello7",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "CELLO🗿😎"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=the_cello7",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "the_cello7"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=the_cello7",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "ke 10 cuyyyy🗿😁"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=the_cello7",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=the_cello7",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=the_cello7",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "12m ago"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=urungkan21",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "ゆうとみんと"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=urungkan21",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "urungkan21"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=urungkan21",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "gue yang Habis WD25jt 😹\ntangkap Anisss 😡\nyones Mana yones 😌"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=urungkan21",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436#comment_by=urungkan21",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "13m ago"
|
||||
}
|
||||
],
|
||||
"savedAt": "2025-07-22T12:50:59.774Z",
|
||||
"type": "comment",
|
||||
"url": "https://www.tiktok.com/@metro_tv/video/7529858656179932436"
|
||||
}
|
||||
]
|
||||
274
example-json/tiktok-profile.json
Normal file
274
example-json/tiktok-profile.json
Normal file
@@ -0,0 +1,274 @@
|
||||
[
|
||||
{
|
||||
"facts": [
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "metro_tv"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "Metro TV"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv",
|
||||
"name": "description",
|
||||
"type": "string",
|
||||
"value": "The Official Account of Metro TV"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv",
|
||||
"name": "image",
|
||||
"type": "url",
|
||||
"value": "https://p16-sign-sg.tiktokcdn.com/tos-alisg-avt-0068/7312074011570733058~tplv-tiktokx-cropcenter:1080:1080.jpeg?dr=14579&refresh_token=71d036a5&x-expires=1753358400&x-signature=eW0JTtwT%2BGOp5MhV%2FNuRCE9tDK8%3D&t=4d5b0474&ps=13740610&shp=a5d48078&shcp=81f88b70&idc=my2"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv",
|
||||
"name": "followerCount",
|
||||
"type": "number",
|
||||
"value": "8300000"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv",
|
||||
"name": "followingCount",
|
||||
"type": "number",
|
||||
"value": "5"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv",
|
||||
"name": "postCount",
|
||||
"type": "number",
|
||||
"value": "15700"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "260400000"
|
||||
}
|
||||
],
|
||||
"savedAt": "2025-07-22T12:50:22.676Z",
|
||||
"type": "profile",
|
||||
"url": "https://www.tiktok.com/@metro_tv"
|
||||
},
|
||||
{
|
||||
"facts": [
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529876715401743624",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529876715401743624"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858656179932436",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529858656179932436"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529877544443596052",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529877544443596052"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529876838890441991",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529876838890441991"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529876619029237013",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529876619029237013"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529873788788378886",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529873788788378886"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529868651105651975",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529868651105651975"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858317242469639",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529858317242469639"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858529126026516",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529858529126026516"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529858036026985749",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529858036026985749"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529857576016612613",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529857576016612613"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529848682011708688",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529848682011708688"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529843661165563143",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529843661165563143"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529843648142363905",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529843648142363905"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529843258269224209",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529843258269224209"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529843252602686741",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529843252602686741"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529827885771705620",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529827885771705620"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529827374465076501",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529827374465076501"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529812013032246548",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529812013032246548"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529805928460209429",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529805928460209429"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529827622180703509",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529827622180703509"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529826652558298385",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529826652558298385"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529825206412561670",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529825206412561670"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529821248117820688",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529821248117820688"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529818903556541717",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529818903556541717"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529805726156426514",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529805726156426514"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529754318732414229",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529754318732414229"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529805514620947733",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529805514620947733"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529807347103862024",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529807347103862024"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529796779689184532",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529796779689184532"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529752657255288084",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529752657255288084"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529796521202666772",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529796521202666772"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529789760177442104",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529789760177442104"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529752692411878677",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529752692411878677"
|
||||
},
|
||||
{
|
||||
"context": "https://www.tiktok.com/@metro_tv/video/7529756053068631312",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://www.tiktok.com/@metro_tv/video/7529756053068631312"
|
||||
}
|
||||
],
|
||||
"savedAt": "2025-07-22T12:50:22.680Z",
|
||||
"type": "post",
|
||||
"url": "https://www.tiktok.com/@metro_tv"
|
||||
}
|
||||
]
|
||||
292
example-json/twitter-post.json
Normal file
292
example-json/twitter-post.json
Normal file
@@ -0,0 +1,292 @@
|
||||
[
|
||||
{
|
||||
"facts": [
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1947327953580196042",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://x.com/jokowi/status/1947327953580196042"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1947327953580196042",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "Joko Widodo"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1947327953580196042",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "jokowi"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1947327953580196042",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Saya melepas kepulangan Bapak Presiden Prabowo di Bandara Adi Soemarmo. Semoga lancar dan aman sampai tujuan. 1:41 1:41"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1947327953580196042",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "1389"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1947327953580196042",
|
||||
"name": "shareCount",
|
||||
"type": "number",
|
||||
"value": "176"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1947327953580196042",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "282"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1947327953580196042",
|
||||
"name": "bookmarkCount",
|
||||
"type": "number",
|
||||
"value": "26"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1947327953580196042",
|
||||
"name": "viewCount",
|
||||
"type": "number",
|
||||
"value": "111728"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1947327953580196042",
|
||||
"name": "createdAt",
|
||||
"type": "datetime",
|
||||
"value": "2025-07-21T16:08:58.000Z"
|
||||
}
|
||||
],
|
||||
"savedAt": "2025-07-22T12:49:01.646Z",
|
||||
"type": "post",
|
||||
"url": "https://x.com/jokowi/status/1947327953580196042"
|
||||
},
|
||||
{
|
||||
"facts": [
|
||||
{
|
||||
"context": "https://x.com/megapand137/status/1947637749789683895",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://x.com/megapand137/status/1947637749789683895"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/megapand137/status/1947637749789683895",
|
||||
"name": "createdAt",
|
||||
"type": "datetime",
|
||||
"value": "2025-07-22T12:39:59.000Z"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/megapand137/status/1947637749789683895",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "Megapand137"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/megapand137/status/1947637749789683895",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "megapand137"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/megapand137/status/1947637749789683895",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Jokowi merasa dihina sehina hinanya katanya atas polemik Ijazahnya...Bukannya bapak selama jadi Presiden sering melakukan pekerjaan pekerjaan hina pak ? Contoh kecilnya...Ngomongnya \" apa \" , tapi yg dikerjakan \"apa \" Apakah iyu tidak hina ?"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/megapand137/status/1947637749789683895",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "2"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/megapand137/status/1947637749789683895",
|
||||
"name": "shareCount",
|
||||
"type": "number",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/megapand137/status/1947637749789683895",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/megapand137/status/1947637749789683895",
|
||||
"name": "viewCount",
|
||||
"type": "number",
|
||||
"value": "69"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/Rojikin14775945/status/1947633517837619228",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://x.com/Rojikin14775945/status/1947633517837619228"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/Rojikin14775945/status/1947633517837619228",
|
||||
"name": "createdAt",
|
||||
"type": "datetime",
|
||||
"value": "2025-07-22T12:23:10.000Z"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/Rojikin14775945/status/1947633517837619228",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "Izasah Palsu"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/Rojikin14775945/status/1947633517837619228",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "Rojikin14775945"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/Rojikin14775945/status/1947633517837619228",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Wi lu kapan digantung atas kebohonganya"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/Rojikin14775945/status/1947633517837619228",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/Rojikin14775945/status/1947633517837619228",
|
||||
"name": "shareCount",
|
||||
"type": "number",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/Rojikin14775945/status/1947633517837619228",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/Rojikin14775945/status/1947633517837619228",
|
||||
"name": "viewCount",
|
||||
"type": "number",
|
||||
"value": "176"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/LukkiLw/status/1947627219519738173",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://x.com/LukkiLw/status/1947627219519738173"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/LukkiLw/status/1947627219519738173",
|
||||
"name": "createdAt",
|
||||
"type": "datetime",
|
||||
"value": "2025-07-22T11:58:08.000Z"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/LukkiLw/status/1947627219519738173",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "Lukki Lw"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/LukkiLw/status/1947627219519738173",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "LukkiLw"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/LukkiLw/status/1947627219519738173",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "kupas tuntas 'kebaikan joko Widodo\n\n[@jokowi](/jokowi)"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/LukkiLw/status/1947627219519738173",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/LukkiLw/status/1947627219519738173",
|
||||
"name": "shareCount",
|
||||
"type": "number",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/LukkiLw/status/1947627219519738173",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/LukkiLw/status/1947627219519738173",
|
||||
"name": "viewCount",
|
||||
"type": "number",
|
||||
"value": "593"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/irmawindya/status/1947391677691490518",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://x.com/irmawindya/status/1947391677691490518"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/irmawindya/status/1947391677691490518",
|
||||
"name": "createdAt",
|
||||
"type": "datetime",
|
||||
"value": "2025-07-21T20:22:11.000Z"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/irmawindya/status/1947391677691490518",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "/juːˈnɔɪ.ə/"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/irmawindya/status/1947391677691490518",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "irmawindya"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/irmawindya/status/1947391677691490518",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": " "
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/irmawindya/status/1947391677691490518",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "156"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/irmawindya/status/1947391677691490518",
|
||||
"name": "shareCount",
|
||||
"type": "number",
|
||||
"value": "24"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/irmawindya/status/1947391677691490518",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "4"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/irmawindya/status/1947391677691490518",
|
||||
"name": "viewCount",
|
||||
"type": "number",
|
||||
"value": "7793"
|
||||
}
|
||||
],
|
||||
"savedAt": "2025-07-22T12:49:01.652Z",
|
||||
"type": "comment",
|
||||
"url": "https://x.com/jokowi/status/1947327953580196042"
|
||||
}
|
||||
]
|
||||
240
example-json/twitter-profile.json
Normal file
240
example-json/twitter-profile.json
Normal file
@@ -0,0 +1,240 @@
|
||||
|
||||
|
||||
[
|
||||
{
|
||||
"facts": [
|
||||
{
|
||||
"context": "https://x.com/jokowi",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "jokowi"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "Joko Widodo"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi",
|
||||
"name": "description",
|
||||
"type": "string",
|
||||
"value": "Akun Twitter resmi Joko Widodo 🇮🇩."
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi",
|
||||
"name": "profileImage",
|
||||
"type": "url",
|
||||
"value": "https://pbs.twimg.com/profile_images/1646769127493877761/bGdslGTd_400x400.jpg"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi",
|
||||
"name": "followerCount",
|
||||
"type": "number",
|
||||
"value": "21905953"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi",
|
||||
"name": "followingCount",
|
||||
"type": "number",
|
||||
"value": "59"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi",
|
||||
"name": "postCount",
|
||||
"type": "number",
|
||||
"value": "7025"
|
||||
}
|
||||
],
|
||||
"savedAt": "2025-07-22T12:48:10.097Z",
|
||||
"type": "profile",
|
||||
"url": "https://x.com/jokowi"
|
||||
},
|
||||
{
|
||||
"facts": [
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1947327953580196042",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://x.com/jokowi/status/1947327953580196042"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1947327953580196042",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "Joko Widodo"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1947327953580196042",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "jokowi"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1947327953580196042",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Saya melepas kepulangan Bapak Presiden Prabowo di Bandara Adi Soemarmo. Semoga lancar dan aman sampai tujuan.  "
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1947327953580196042",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "1388"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1947327953580196042",
|
||||
"name": "shareCount",
|
||||
"type": "number",
|
||||
"value": "176"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1947327953580196042",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "282"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1947327953580196042",
|
||||
"name": "bookmarkCount",
|
||||
"type": "number",
|
||||
"value": "26"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1947327953580196042",
|
||||
"name": "viewCount",
|
||||
"type": "number",
|
||||
"value": "111669"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1947327953580196042",
|
||||
"name": "createdAt",
|
||||
"type": "datetime",
|
||||
"value": "2025-07-21T16:08:58.000Z"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1947194422426517998",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://x.com/jokowi/status/1947194422426517998"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1947194422426517998",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "Joko Widodo"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1947194422426517998",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "jokowi"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1947194422426517998",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Saya mengajak Bapak Presiden Prabowo menikmati hidangan Bakmi Bu Citro di Solo. Rumah makan sederhana yang punya rasa istimewa. Kami duduk santai, berbincang ringan ditemani semangkuk bakmi hangat. Sebuah momen personal, jauh dari agenda kenegaraan, dan berkesan. Terima kasih Pak Presiden  "
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1947194422426517998",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "1755"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1947194422426517998",
|
||||
"name": "shareCount",
|
||||
"type": "number",
|
||||
"value": "253"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1947194422426517998",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "265"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1947194422426517998",
|
||||
"name": "bookmarkCount",
|
||||
"type": "number",
|
||||
"value": "43"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1947194422426517998",
|
||||
"name": "viewCount",
|
||||
"type": "number",
|
||||
"value": "134879"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1947194422426517998",
|
||||
"name": "createdAt",
|
||||
"type": "datetime",
|
||||
"value": "2025-07-21T07:18:22.000Z"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1946955377771450714",
|
||||
"name": "url",
|
||||
"type": "url",
|
||||
"value": "https://x.com/jokowi/status/1946955377771450714"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1946955377771450714",
|
||||
"name": "displayName",
|
||||
"type": "string",
|
||||
"value": "Joko Widodo"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1946955377771450714",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "jokowi"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1946955377771450714",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Terima kasih Bapak Presiden Prabowo yang telah berkenan berkunjung kerumah keluarga kami di Solo  "
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1946955377771450714",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "3117"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1946955377771450714",
|
||||
"name": "shareCount",
|
||||
"type": "number",
|
||||
"value": "534"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1946955377771450714",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "355"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1946955377771450714",
|
||||
"name": "bookmarkCount",
|
||||
"type": "number",
|
||||
"value": "81"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1946955377771450714",
|
||||
"name": "viewCount",
|
||||
"type": "number",
|
||||
"value": "335996"
|
||||
},
|
||||
{
|
||||
"context": "https://x.com/jokowi/status/1946955377771450714",
|
||||
"name": "createdAt",
|
||||
"type": "datetime",
|
||||
"value": "2025-07-20T15:28:29.000Z"
|
||||
}
|
||||
],
|
||||
"savedAt": "2025-07-22T12:48:10.098Z",
|
||||
"type": "post",
|
||||
"url": "https://x.com/jokowi"
|
||||
}
|
||||
]
|
||||
664
example-json/youtube-post.json
Normal file
664
example-json/youtube-post.json
Normal file
@@ -0,0 +1,664 @@
|
||||
[
|
||||
{
|
||||
"facts": [
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o",
|
||||
"name": "title",
|
||||
"type": "string",
|
||||
"value": "Kumpulan Skak Balik Jokowi ke Prabowo di Debat Keempat Pilpres 2019"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o",
|
||||
"name": "description",
|
||||
"type": "string",
|
||||
"value": "Untuk melihat video-video menarik lainnya kunjungi: https://video.medcom.id/ Dua kandidat capres Joko Widodo dan Prabowo Subianto beradu gagasan dalam debat keempat Pilpres 2019. Debat kali ini mengusung tema ideologi, pemerintahan, keamanan serta hubungan internasional. Siapa unggul pada debat kali ini?"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o",
|
||||
"name": "date",
|
||||
"type": "datetime",
|
||||
"value": "2019-03-30T11:41:18-07:00"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o",
|
||||
"name": "duration",
|
||||
"type": "timestr",
|
||||
"value": "PT1303S"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o",
|
||||
"name": "thumbnail",
|
||||
"type": "url",
|
||||
"value": "https://i.ytimg.com/vi/_joLYHj0i3o/hqdefault.jpg"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o",
|
||||
"name": "viewCount",
|
||||
"type": "number",
|
||||
"value": "10535799"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "94857"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "32029"
|
||||
}
|
||||
],
|
||||
"savedAt": "2025-07-22T12:57:02.567Z",
|
||||
"type": "post",
|
||||
"url": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV"
|
||||
},
|
||||
{
|
||||
"facts": [
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@hambaallah5591",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "@hambaallah5591"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@hambaallah5591",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Jangan lupa p prawono tgl 17april hadir dalam pelantikan bpk jokowi untuk 2 preode yang setuju mana like nya"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@hambaallah5591",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "467"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@hambaallah5591",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "8"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@hambaallah5591",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "6 tahun yang lalu"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@aztycancer7759",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "@aztycancer7759"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@aztycancer7759",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Hhhhhhhh secara tdk langsung mengakui klo Dia non.....???.\nMulai Emosi ya pak..... Smgt Bpk jokowi rakyat yg cerdas pasti mendukung bpk"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@aztycancer7759",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "356"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@aztycancer7759",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "10"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@aztycancer7759",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "6 tahun yang lalu"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@sasmithanadra3103",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "@sasmithanadra3103"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@sasmithanadra3103",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Inti Debat ,\nPrabowo Maha benar,kemudiaan yg dia lakukan menyalahkan Pak Jokowi.\nKarena itu,sbg Imbalan.\nSaya Coblos No.01\nYg setuju 1..\n#LIKE 🖒"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@sasmithanadra3103",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "205"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@sasmithanadra3103",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "2"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@sasmithanadra3103",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "6 tahun yang lalu"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@عرفانالزهري",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "@عرفانالزهري"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@عرفانالزهري",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Siapa yg disini mampir lagi setelah melihat debat semalam"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@عرفانالزهري",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "84"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@عرفانالزهري",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "7"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@عرفانالزهري",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "1 tahun yang lalu"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@naylaandini8808",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "@naylaandini8808"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@naylaandini8808",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Jokowi = like\nPrabowo = komen"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@naylaandini8808",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "7200"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@naylaandini8808",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "116"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@naylaandini8808",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "6 tahun yang lalu"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@andrialdytaqwa9291",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "@andrialdytaqwa9291"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@andrialdytaqwa9291",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "militer indonesia masuk 15 besar di dunia masih dibilang rapuh\n\n\nlike kalau setuju"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@andrialdytaqwa9291",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "338"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@andrialdytaqwa9291",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "19"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@andrialdytaqwa9291",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "6 tahun yang lalu"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@pegijasnil1596",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "@pegijasnil1596"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@pegijasnil1596",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Pak jokowi memang cocok jadi presiden...soalnya pak jokowi membuat nengara ini semakin semangat maju. Maaf pak dulu saya gak milih bapak tapi sakarang saya pilih bapak"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@pegijasnil1596",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "260"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@pegijasnil1596",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "2"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@pegijasnil1596",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "6 tahun yang lalu"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@davidabhafidz3556",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "@davidabhafidz3556"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@davidabhafidz3556",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Ada ga yg nonton ini di bulan januari 2024"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@davidabhafidz3556",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "121"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@davidabhafidz3556",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "35"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@davidabhafidz3556",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "1 tahun yang lalu"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@merchandisingawesome2600",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "@merchandisingawesome2600"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@merchandisingawesome2600",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Ini seperti curhatan warga ke president \nWarga = Prabowo\nPresiden = Pak Jokowi"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@merchandisingawesome2600",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "138"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@merchandisingawesome2600",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "5"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@merchandisingawesome2600",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "6 tahun yang lalu"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@rezanajwa9999",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "@rezanajwa9999"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@rezanajwa9999",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Prabowo terus bicara masa lalunya(bicata ttg tentara terus).sementara jokowi bicara ttg pencapaian yg sdh di lakukan juga bicara masa depan..sangat kontras...(sy tdk mendengar jokowi bicara ttg usahanya)"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@rezanajwa9999",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "373"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@rezanajwa9999",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "13"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@rezanajwa9999",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "6 tahun yang lalu"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@sukasukabro1137",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "@sukasukabro1137"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@sukasukabro1137",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "saya nangkapnya kog, malah prabowo bukan memberi gagasan untuk indonesia, tapi menyangkal semua gagasan/ ide2 nya pak jokowi... semua nya di sangkal , di salahkan, tnpa membrikan gagasan yg lebih baik\n\nyg setuju like"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@sukasukabro1137",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "156"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@sukasukabro1137",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "7"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@sukasukabro1137",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "6 tahun yang lalu"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@alfarezialamsyah3975",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "@alfarezialamsyah3975"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@alfarezialamsyah3975",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Ini yang namanya debat . Ngga kaya debat tahun 2024 yang menjatuhkan dan merendahkan."
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@alfarezialamsyah3975",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "17"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@alfarezialamsyah3975",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@alfarezialamsyah3975",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "1 tahun yang lalu"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@IKEWAKI666",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "@IKEWAKI666"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@IKEWAKI666",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Pak Wowo bahasanya muter\" diperang trs\nPak owi bahasnya sana sini lebih luas :v\nTrs ku lihat keknya Prabowo gk mikir buat kedepan nya bakal gimana kalo lakuin itu,\nPak Jokowi lebih bekerjasama dengan negara lain lebih berteman,pak Prabowo malah pengen Indonesia itu kek lakuin segalanya sendiri....kapan negara kita maju :v kapan negara kita bersaing kalo Indonesia cuma melakukan segalanya sendiri :v\nDunia ini bukan cuma perang :v"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@IKEWAKI666",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "290"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@IKEWAKI666",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "13"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@IKEWAKI666",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "6 tahun yang lalu (diedit)"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@rafafebryan5626",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "@rafafebryan5626"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@rafafebryan5626",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Ada 3 golongan yg susah di nasehati\n\n1. Orang yg sedang jatuh cinta \n2. Orang yg sedang emosi\n3. #02"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@rafafebryan5626",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "647"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@rafafebryan5626",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "33"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@rafafebryan5626",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "6 tahun yang lalu"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@sarajevo2469",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "@sarajevo2469"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@sarajevo2469",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Tahun 1975 pak prabowo bilang ikut perang ke timor timor, gak salah pak!! Bukanya bapak wktu itu malah diasingkan ke bali, itu fakta lhoo pak"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@sarajevo2469",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "460"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@sarajevo2469",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "9"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@sarajevo2469",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "6 tahun yang lalu"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@LFchannel-bb4uu",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "@LFchannel-bb4uu"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@LFchannel-bb4uu",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Hebatnya pak Jokowi..memilih pak prabowo sebagai menhanx yang benar2 mengetahui kondisi pertahanan dan keamanan negara kita...dan itu di buktikan oleh pak prabowo selama 5 tahun.. dan pak prabowo jg mengakui banyak belajar dr pak jokowi...mereka ini benar2 orang hebat yg di ciptakan untuk indonesia 🫡"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@LFchannel-bb4uu",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "36"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@LFchannel-bb4uu",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "3"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@LFchannel-bb4uu",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "1 tahun yang lalu"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@cici3644",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "@cici3644"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@cici3644",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "pilih prabowo hancur ~> komen\npilih jokowi makmur ~> like\n\nRakyat indonesia makin cerdas"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@cici3644",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "3200"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@cici3644",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "84"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@cici3644",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "6 tahun yang lalu"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@surya247766",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "@surya247766"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@surya247766",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Beda kelas brow... Meskipun pak dhe dibilang plonga plongo tapi kecerdasannya melebihi rivalnya..."
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@surya247766",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "3500"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@surya247766",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "204"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@surya247766",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "6 tahun yang lalu"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@hizkiakurniawan",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "@hizkiakurniawan"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@hizkiakurniawan",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "Prabowo: saya, saya, saya, saya\nJokowi: kita, kita, kita, kita\nKeliatan yang mana yang mikirin diri sendiri"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@hizkiakurniawan",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "2500"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@hizkiakurniawan",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "52"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@hizkiakurniawan",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "6 tahun yang lalu"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@chintyaayumustika8842",
|
||||
"name": "username",
|
||||
"type": "string",
|
||||
"value": "@chintyaayumustika8842"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@chintyaayumustika8842",
|
||||
"name": "content",
|
||||
"type": "string",
|
||||
"value": "rindu debat ini , mereka berdua saat ini terbukti orang yang baik , sehat selalu pak jokowi dan pak prabowo"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@chintyaayumustika8842",
|
||||
"name": "likeCount",
|
||||
"type": "number",
|
||||
"value": "75"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@chintyaayumustika8842",
|
||||
"name": "commentCount",
|
||||
"type": "number",
|
||||
"value": "6"
|
||||
},
|
||||
{
|
||||
"context": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV#comment_by=@chintyaayumustika8842",
|
||||
"name": "createdAt",
|
||||
"type": "timestr",
|
||||
"value": "1 tahun yang lalu"
|
||||
}
|
||||
],
|
||||
"savedAt": "2025-07-22T12:57:02.571Z",
|
||||
"type": "comment",
|
||||
"url": "https://www.youtube.com/watch?v=_joLYHj0i3o&ab_channel=METROTV"
|
||||
}
|
||||
]
|
||||
1066
example-json/youtube-profile.json
Normal file
1066
example-json/youtube-profile.json
Normal file
File diff suppressed because it is too large
Load Diff
1797
package-lock.json
generated
Normal file
1797
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
package.json
Normal file
21
package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "new-browser-input",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"@types/chrome": "^0.0.326",
|
||||
"@typescript-eslint/eslint-plugin": "^8.34.0",
|
||||
"@typescript-eslint/parser": "^8.34.0",
|
||||
"eslint": "^9.28.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/turndown": "^5.0.5",
|
||||
"turndown": "^7.2.0",
|
||||
"turndown-plugin-gfm": "^1.0.2"
|
||||
}
|
||||
}
|
||||
BIN
public/extension.zip
Normal file
BIN
public/extension.zip
Normal file
Binary file not shown.
51
resultFacebook.js
Normal file
51
resultFacebook.js
Normal file
@@ -0,0 +1,51 @@
|
||||
[
|
||||
{
|
||||
"facts": [
|
||||
{
|
||||
"context": "https://www.facebook.com",
|
||||
"name": "fb_post_source_url",
|
||||
"type": "url",
|
||||
"value": "https://www.facebook.com"
|
||||
},
|
||||
{
|
||||
"context": "https://www.facebook.com",
|
||||
"name": "fb_post_profile_url",
|
||||
"type": "url",
|
||||
"value": "https://www.facebook.com/KOMPAScom"
|
||||
},
|
||||
{
|
||||
"context": "https://www.facebook.com",
|
||||
"name": "fb_post_display_name",
|
||||
"type": "name",
|
||||
"value": "Kompas.com"
|
||||
},
|
||||
{
|
||||
"context": "https://www.facebook.com",
|
||||
"name": "fb_post_content_md",
|
||||
"type": "md",
|
||||
"value": "[photo](https://scontent.fcgk18-1.fna.fbcdn.net/v/t39.30808-6/552480808_1307882211367890_924495273445433584_n.jpg?stp=dst-jpg_p526x296_tt6&_nc_cat=111&ccb=1-7&_nc_sid=127cfc&_nc_ohc=rmt_4eIvftYQ7kNvwGDecy3&_nc_oc=Adm4VTjN2xcbzrjqarwQfh5wArl6OaDDOgge44bwbwu0mLE98HEo_LPjTey2sV0LdR0&_nc_zt=23&_nc_ht=scontent.fcgk18-1.fna&_nc_gid=pbxoFcISGk0l9vQ7hd4buA&oh=00_AfYyGjiCbgz-zfzTKp3PpfjEbbNNpJuy-9OaKb9v4juRdA&oe=68DC6CFD)\n\nPresiden RI Prabowo Subianto menyatakan Indonesia akan mengakui Israel bila Israel mengakui Palestina.\n\nBaca selengkapnya \n\n[https://nasional.kompas.com/.../prabowo-di-ktt-pbb-jika...](https://l.facebook.com/l.php?u=https%3A%2F%2Fnasional.kompas.com%2Fread%2F2025%2F09%2F23%2F05583471%2Fprabowo-di-ktt-pbb-jika-israel-akui-palestina-ri-akui-israel%3Ffbclid%3DIwZXh0bgNhZW0CMTAAYnJpZBExMGVEc0Y0eGxpdGdFcE5IQwEeY5f-Tnn1TkSGDkCAf9jxR_RaW2RZ_3eGhCiWBqDNMh9tPrSqhpvkaFhZioE_aem_8X5jz7VulHNDODRDBDC2cg&h=AT2u2bZowgyWIp1xOwLXROAsM_lcjBzkuGfLKKW6LCSoV-IOzOX8FSJGw96PcCaAG-YS4DP56vMdbVPzT7eiO8XcZO3kYYZ3Ewhz14uNm1e1DGm-CPAUaufbKpdsY4nXFIz3fBvl&__tn__=-UK-R&c[0]=AT3_uBBuaqdxsDtcTjjYawf8809vYPadik6-z9Yzi-N6-vQk7SiN8BHlTARL6iWFy1OHDAKqF9be7khVqDBUwjBLM8-4Jzxffgp5KVuzioPJZNRS1423JuqTb1zFMdzZFXhh4paVd8ZdppQpMOusBoNJSqa5SmxwZlTo99n9lnWaY2CdW6WYnIgXLWcr3AGS1tQOmNn6OqprGAu8p4Cg08s86w)\n\n~LL [#Palestina](https://www.facebook.com/hashtag/palestina?__cft__[0]=AZVIekghHa8vSfH_P_pyOdhfBlcsgze3ax0vSHyQF_kQfC4UMzScNJ0un_oGTcS2oT8CUCeiKEQNURVZW_ZdWdErLCTqfK1g-dziSlyk2J1mLbZsaFKZvbto7CADJCqITYqSZ4MHoa6fFH_PCYQ2NbJ1nYY1V5iIepUval71zIAiPyptkTlNxErtcxGAaMpQNaI&__tn__=*NK-R) [#Prabowo](https://www.facebook.com/hashtag/prabowo?__cft__[0]=AZVIekghHa8vSfH_P_pyOdhfBlcsgze3ax0vSHyQF_kQfC4UMzScNJ0un_oGTcS2oT8CUCeiKEQNURVZW_ZdWdErLCTqfK1g-dziSlyk2J1mLbZsaFKZvbto7CADJCqITYqSZ4MHoa6fFH_PCYQ2NbJ1nYY1V5iIepUval71zIAiPyptkTlNxErtcxGAaMpQNaI&__tn__=*NK-R)"
|
||||
},
|
||||
{
|
||||
"context": "https://www.facebook.com",
|
||||
"name": "fb_post_like_count",
|
||||
"type": "integer",
|
||||
"value": "15000"
|
||||
},
|
||||
{
|
||||
"context": "https://www.facebook.com",
|
||||
"name": "fb_post_comment_count",
|
||||
"type": "integer",
|
||||
"value": "7500"
|
||||
},
|
||||
{
|
||||
"context": "https://www.facebook.com",
|
||||
"name": "fb_post_share_count",
|
||||
"type": "integer",
|
||||
"value": "2000"
|
||||
}
|
||||
],
|
||||
"savedAt": "2025-09-26T15:01:59.619Z",
|
||||
"type": "post",
|
||||
"url": "https://www.facebook.com/search/top?q=prabowo"
|
||||
}
|
||||
]
|
||||
128
src/background.ts
Normal file
128
src/background.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
// You can perform any setup tasks here, like creating default settings
|
||||
}
|
||||
);
|
||||
|
||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
if (request.action === 'saveData') {
|
||||
interface SaveDataItem {
|
||||
type: string;
|
||||
url: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const dataArray: SaveDataItem[] = request.data as SaveDataItem[];
|
||||
|
||||
console.log('Received data for saving:', dataArray);
|
||||
|
||||
|
||||
dataArray.forEach((item: SaveDataItem) => {
|
||||
sendDataToServer(item, item.type, item.url);
|
||||
});
|
||||
|
||||
async function sendDataToServer(data: any, type: string, url: string) {
|
||||
if (!data || !type) {
|
||||
console.error('Invalid data or type for sending to server:', { data, type });
|
||||
return;
|
||||
}
|
||||
|
||||
var resolvedUrl = await resolveUrl(type, url);
|
||||
|
||||
console.log(`Sending data to ${resolvedUrl}:`);
|
||||
|
||||
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json, application/problem+json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
};
|
||||
|
||||
try {
|
||||
console.log(`Sending data to ${resolvedUrl}:`);
|
||||
|
||||
const response = await fetch(resolvedUrl, options).then(res => {
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP error! status: ${res.status}`);
|
||||
}
|
||||
return res;
|
||||
});
|
||||
const responseData = await response.json();
|
||||
console.log(responseData);
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function resolveUrl(type: string, platform: string): Promise<string> {
|
||||
const platformMap: Record<string, string> = {
|
||||
"facebook.com": 'fb',
|
||||
"instagram.com": 'ig',
|
||||
"youtube.com": 'yt',
|
||||
"linkedin.com": 'li',
|
||||
"x.com": 'x',
|
||||
"tiktok.com": 'tt'
|
||||
};
|
||||
|
||||
// Get apiURL from storage asynchronously
|
||||
const apiUrl = await new Promise<string>((resolve) => {
|
||||
chrome.storage.local.get('apiURL', (result) => {
|
||||
const url = result.apiURL || 'https://dropper.nakhari.us/';
|
||||
console.log('Retrieved apiURL from storage:', url);
|
||||
resolve(url);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// clean https://instagram.com/p/DMVZrA6hg4s/
|
||||
// https://instagram.com/
|
||||
const cleanedUrl = platform.replace(/https?:\/\//, '').replace(/www\./, '').split('/')[0];
|
||||
|
||||
const baseUrls: Record<string, string> = {
|
||||
profile: `${apiUrl}${platformMap[cleanedUrl]}/profile`,
|
||||
post: `${apiUrl}${platformMap[cleanedUrl]}/post`,
|
||||
comment: `${apiUrl}${platformMap[cleanedUrl]}/comment`,
|
||||
};
|
||||
|
||||
console.log('Final URL:', baseUrls[type]);
|
||||
return baseUrls[type];
|
||||
}
|
||||
|
||||
// Save to chrome.storage.session (Chrome 102+)
|
||||
if (chrome.storage && chrome.storage.session) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const key = `session_data_${timestamp}`;
|
||||
|
||||
chrome.storage.session.set({ [key]: request.data }, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error('Error saving to session storage:', chrome.runtime.lastError);
|
||||
sendResponse({ success: false, error: chrome.runtime.lastError.message });
|
||||
} else {
|
||||
// Kirim data kembali untuk ditampilkan otomatis
|
||||
sendResponse({ success: true, key: key, data: request.data });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Fallback ke local storage jika session storage tidak tersedia
|
||||
console.warn('Session storage not available, using local storage');
|
||||
const timestamp = new Date().toISOString();
|
||||
const key = `local_data_${timestamp}`;
|
||||
|
||||
chrome.storage.local.set({ [key]: request.data }, () => {
|
||||
if (chrome.runtime.lastError) {
|
||||
sendResponse({ success: false, error: chrome.runtime.lastError.message });
|
||||
} else {
|
||||
// Kirim data kembali untuk ditampilkan otomatis
|
||||
sendResponse({ success: true, key: key, data: request.data });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return true; // Important for async response
|
||||
}
|
||||
});
|
||||
259
src/contentScript.ts
Normal file
259
src/contentScript.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
import XModule from './modules/x';
|
||||
import TiktokModule from './modules/tiktok';
|
||||
import LinkedInModule from './modules/linkedin';
|
||||
import YTModule from './modules/youtube';
|
||||
import FacebookModule from './modules/facebook';
|
||||
import InstagramModule from './modules/instagram';
|
||||
|
||||
|
||||
|
||||
function getActiveModule() {
|
||||
const hostname = window.location.hostname;
|
||||
if (hostname.includes('tiktok.com')) return new TiktokModule();
|
||||
if (hostname.includes('x.com')) return new XModule();
|
||||
if (hostname.includes('linkedin.com')) return new LinkedInModule();
|
||||
if (hostname.includes('youtube.com')) return new YTModule();
|
||||
if (hostname.includes('facebook.com')) return new FacebookModule();
|
||||
if (hostname.includes('instagram.com')) return new InstagramModule();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function createToggleButton(): void {
|
||||
// Hindari duplikat
|
||||
if (document.getElementById('my-ext-toggle-btn')) return;
|
||||
|
||||
const btn = document.createElement('button');
|
||||
btn.id = 'my-ext-toggle-btn';
|
||||
btn.textContent = '<';
|
||||
btn.style.cssText = `
|
||||
position: fixed;
|
||||
top: 100px;
|
||||
right: 0;
|
||||
z-index: 2147483646;;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #333;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
btn.addEventListener('click', () => {
|
||||
const sidebar = document.getElementById('my-extension-sidepanel');
|
||||
if (sidebar) {
|
||||
sidebar.remove();
|
||||
} else {
|
||||
createSidebarPanel();
|
||||
}
|
||||
});
|
||||
|
||||
document.body.appendChild(btn);
|
||||
}
|
||||
|
||||
const activeModule = getActiveModule();
|
||||
|
||||
// Track URL changes for SPA navigation
|
||||
let currentUrl = window.location.href;
|
||||
|
||||
function reloadSidebarOnUrlChange(): void {
|
||||
// Check for URL changes every 500ms
|
||||
setInterval(() => {
|
||||
if (window.location.href !== currentUrl) {
|
||||
currentUrl = window.location.href;
|
||||
console.log('URL changed, reloading sidebar...');
|
||||
|
||||
// Remove existing sidebar
|
||||
const existingSidebar = document.getElementById('my-extension-sidepanel');
|
||||
if (existingSidebar) {
|
||||
existingSidebar.remove();
|
||||
}
|
||||
|
||||
// Create new sidebar after a small delay to ensure page content is loaded
|
||||
setTimeout(() => {
|
||||
createSidebarPanel();
|
||||
}, 500);
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Also listen for browser navigation events
|
||||
// window.addEventListener('popstate', () => {
|
||||
// console.log('Browser navigation detected, reloading sidebar...');
|
||||
|
||||
// setTimeout(() => {
|
||||
// const existingSidebar = document.getElementById('my-extension-sidepanel');
|
||||
// if (existingSidebar) {
|
||||
// existingSidebar.remove();
|
||||
// }
|
||||
// createSidebarPanel();
|
||||
// }, 500);
|
||||
// });
|
||||
|
||||
// Listen for pushstate/replacestate (used by SPAs)
|
||||
const originalPushState = history.pushState;
|
||||
const originalReplaceState = history.replaceState;
|
||||
|
||||
// history.pushState = function (...args) {
|
||||
// originalPushState.apply(history, args);
|
||||
// setTimeout(() => {
|
||||
// console.log('SPA navigation detected (pushState), reloading sidebar...');
|
||||
// const existingSidebar = document.getElementById('my-extension-sidepanel');
|
||||
// if (existingSidebar) {
|
||||
// existingSidebar.remove();
|
||||
// }
|
||||
// createSidebarPanel();
|
||||
// }, 500);
|
||||
// };
|
||||
|
||||
// history.replaceState = function (...args) {
|
||||
// originalReplaceState.apply(history, args);
|
||||
// setTimeout(() => {
|
||||
// console.log('SPA navigation detected (replaceState), reloading sidebar...');
|
||||
// const existingSidebar = document.getElementById('my-extension-sidepanel');
|
||||
// if (existingSidebar) {
|
||||
// existingSidebar.remove();
|
||||
// }
|
||||
// createSidebarPanel();
|
||||
// }, 500);
|
||||
// };
|
||||
|
||||
function createSidebarPanel(): void {
|
||||
// Remove existing sidebar if any
|
||||
const existingSidebar = document.getElementById('my-extension-sidepanel');
|
||||
if (existingSidebar) {
|
||||
existingSidebar.remove();
|
||||
}
|
||||
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.id = 'my-extension-sidepanel';
|
||||
iframe.src = chrome.runtime.getURL('popup/popup.html');
|
||||
iframe.style.position = 'fixed';
|
||||
iframe.style.top = '0';
|
||||
iframe.style.right = '0';
|
||||
iframe.style.width = '320px';
|
||||
iframe.style.minWidth = '320px';
|
||||
iframe.style.maxWidth = '320px';
|
||||
iframe.style.height = '100vh';
|
||||
iframe.style.zIndex = '2147483647';
|
||||
iframe.style.border = 'none';
|
||||
iframe.style.background = 'transparent';
|
||||
iframe.style.boxShadow = '0 0 8px rgba(0,0,0,0.15)';
|
||||
iframe.style.display = 'block';
|
||||
|
||||
document.body.appendChild(iframe);
|
||||
|
||||
console.log('Sidebar panel created for:', window.location.href);
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
if (!activeModule) {
|
||||
console.warn('No active module found for this site');
|
||||
sendResponse({ success: false, error: 'No active module found' });
|
||||
return;
|
||||
}
|
||||
|
||||
switch (request.action) {
|
||||
case 'toggleSidebar':
|
||||
const sidebar = document.getElementById('my-extension-sidepanel');
|
||||
if (sidebar) {
|
||||
sidebar.remove();
|
||||
} else {
|
||||
createSidebarPanel();
|
||||
}
|
||||
sendResponse({ success: true });
|
||||
break;
|
||||
|
||||
case 'tagElement':
|
||||
activeModule.tagElement();
|
||||
sendResponse({ success: true });
|
||||
console.log(resolveUrl('profile'));
|
||||
break;
|
||||
case 'saveData':
|
||||
|
||||
// Handle async saveData for TikTok
|
||||
(async () => {
|
||||
try {
|
||||
const data = await activeModule.saveData();
|
||||
|
||||
// data.forEach(item => {
|
||||
// sendDataToServer(item, item.type);
|
||||
// })
|
||||
|
||||
// Send to background for saving
|
||||
chrome.runtime.sendMessage({ action: 'saveData', data }, (response) => {
|
||||
if (response?.success) {
|
||||
// Send data back to popup for automatic display
|
||||
sendResponse({ success: true, data: response.data, key: response.key, saved: true });
|
||||
} else {
|
||||
sendResponse({ success: false, error: 'Failed to save data' });
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in saveData:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
sendResponse({ success: false, error: `Failed to fetch data: ${errorMessage}` });
|
||||
}
|
||||
})();
|
||||
|
||||
return true; // Important: Indicates that the response is sent asynchronously
|
||||
default:
|
||||
sendResponse({ success: false, error: 'Unknown action' });
|
||||
}
|
||||
});
|
||||
|
||||
async function sendDataToServer(data: any, type: string) {
|
||||
if (!data || !type) {
|
||||
console.error('Invalid data or type for sending to server:', { data, type });
|
||||
return;
|
||||
}
|
||||
|
||||
var url = resolveUrl(type);
|
||||
|
||||
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json, application/problem+json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
const data = await response.json();
|
||||
console.log(data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function resolveUrl(type: string): string {
|
||||
|
||||
const platformMap: Record<string, string> = {
|
||||
"www.facebook.com": 'fb',
|
||||
"www.instagram.com": 'ig',
|
||||
"www.youtube.com": 'yt',
|
||||
"www.linkedin.com": 'li',
|
||||
"x.com": 'x',
|
||||
"www.tiktok.com": 'tt'
|
||||
};
|
||||
const apiUrl = 'http://192.168.10.176:8888/'
|
||||
const baseUrls: Record<string, string> = {
|
||||
profile: `${apiUrl + platformMap[window.location.hostname]}/profile`,
|
||||
post: `${apiUrl + platformMap[window.location.hostname]}/post`,
|
||||
comment: `${apiUrl + platformMap[window.location.hostname]}/comment`,
|
||||
};
|
||||
|
||||
return baseUrls[type];
|
||||
}
|
||||
|
||||
|
||||
// Initialize sidebar and start monitoring
|
||||
createSidebarPanel();
|
||||
createToggleButton();
|
||||
// reloadSidebarOnUrlChange();
|
||||
61
src/helper/parseCount.ts
Normal file
61
src/helper/parseCount.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
export function parseCount(value: string): string {
|
||||
if (!value || value.trim() === "") return "0";
|
||||
|
||||
let s = value.toLowerCase().trim();
|
||||
|
||||
// --- 1) Deteksi multiplier (satuan)
|
||||
const units: Record<string, number> = {
|
||||
jt: 1_000_000, juta: 1_000_000, m: 1_000_000, mio: 1_000_000,
|
||||
rb: 1_000, ribu: 1_000, k: 1_000,
|
||||
b: 1_000_000_000, miliar: 1_000_000_000, milyar: 1_000_000_000,
|
||||
};
|
||||
|
||||
let multiplier = 1;
|
||||
for (const [key, mult] of Object.entries(units)) {
|
||||
if (s.includes(key)) {
|
||||
multiplier = mult;
|
||||
// Hapus label satuan agar angka mudah diparse
|
||||
s = s.replace(new RegExp(key, "g"), "");
|
||||
}
|
||||
}
|
||||
|
||||
// --- 2) Normalisasi pemisah ribuan/desimal
|
||||
// Kasus A: punya titik & koma -> anggap '.' ribuan, ',' desimal => buang '.', ganti ',' -> '.'
|
||||
if (s.includes(".") && s.includes(",")) {
|
||||
s = s.replace(/\./g, "").replace(/,/g, ".");
|
||||
} else {
|
||||
// Kasus B: hanya koma
|
||||
const hasOnlyComma = s.includes(",") && !s.includes(".");
|
||||
// Jika ada pola ribuan '1,234' (barat), buang koma.
|
||||
if (hasOnlyComma && /\b\d{1,3}(,\d{3})+\b/.test(s)) {
|
||||
s = s.replace(/,/g, "");
|
||||
} else if (hasOnlyComma) {
|
||||
// Anggap koma = desimal (Indonesia)
|
||||
s = s.replace(/,/g, ".");
|
||||
}
|
||||
// Kasus C: hanya titik -> cek apakah pola ribuan Indonesia '1.234' atau desimal
|
||||
if (!s.includes(",") && s.includes(".")) {
|
||||
if (/\b\d{1,3}(\.\d{3})+\b/.test(s)) {
|
||||
// jelas ribuan -> hapus titik
|
||||
s = s.replace(/\./g, "");
|
||||
}
|
||||
// else: biarkan '.' sebagai desimal
|
||||
}
|
||||
}
|
||||
|
||||
// --- 3) Tangani pola ribuan besar yang tersisa (jaga-jaga)
|
||||
if (/\d+\.\d+\.\d+/.test(s)) {
|
||||
s = s.replace(/[^\d]/g, ""); // ambil digit saja
|
||||
}
|
||||
|
||||
// --- 4) Ambil angka pertama (boleh desimal tunggal)
|
||||
const m = s.match(/(\d+(?:\.\d+)?)/);
|
||||
if (!m) return "0";
|
||||
|
||||
const num = parseFloat(m[1]);
|
||||
if (Number.isNaN(num)) return "0";
|
||||
|
||||
// --- 5) Hasil akhir
|
||||
const out = Math.round(num * multiplier);
|
||||
return out.toString();
|
||||
}
|
||||
10
src/index.html
Normal file
10
src/index.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document</title>
|
||||
</head>
|
||||
<body>
|
||||
<video class="x1lliihq x5yr21d xh8yej3" playsinline="" preload="none" src="blob:https://www.instagram.com/0211cd5e-7ae3-419f-8cdc-bcef367d3007" style="display: block;"></video>
|
||||
</body>
|
||||
</html>
|
||||
63
src/interfaces.ts
Normal file
63
src/interfaces.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
|
||||
interface PostResult {
|
||||
ulid?: string;
|
||||
url: string;
|
||||
post: Post ;
|
||||
uploadedAt: string;
|
||||
comments: Comment[];
|
||||
}
|
||||
|
||||
interface Post {
|
||||
ulid?:string;
|
||||
url:string
|
||||
username?: string;
|
||||
displayName: string;
|
||||
content: string;
|
||||
type: string;
|
||||
likeCount:number;
|
||||
shareCount:number;
|
||||
commentCount:number;
|
||||
viewCount?:number; //tiktok, x
|
||||
bookmarkCount?:number;
|
||||
savedAt?: string; // if format "6h ago"
|
||||
date?: string;
|
||||
}
|
||||
|
||||
interface Comment{
|
||||
ulid?: string;
|
||||
url?:string;
|
||||
username?: string;
|
||||
postUrl:string;
|
||||
displayName: string;
|
||||
content: string;
|
||||
type: string;
|
||||
likeCount:number;
|
||||
shareCount?:number;
|
||||
commentCount:number;
|
||||
savedAt?: string;
|
||||
|
||||
}
|
||||
|
||||
interface ProfileResult {
|
||||
ulid?:string;
|
||||
url: string;
|
||||
type: string;
|
||||
profile: Profile;
|
||||
uploadedAt: string;
|
||||
posts: Post[];
|
||||
}
|
||||
|
||||
interface Profile {
|
||||
ulid?:string;
|
||||
username?: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
image: string;
|
||||
createdAt?: string; // x only
|
||||
followingCount:number;
|
||||
followerCount:number;
|
||||
postCount?:number;
|
||||
likeCount?:number;
|
||||
}
|
||||
|
||||
export type { PostResult, Post, Comment, ProfileResult, Profile };
|
||||
9
src/mark.css
Normal file
9
src/mark.css
Normal file
@@ -0,0 +1,9 @@
|
||||
[class*="tagged"]
|
||||
{
|
||||
border: 2px solid #ff0000 !important;
|
||||
}
|
||||
|
||||
[class*="saved"]
|
||||
{
|
||||
border: 2px solid #00ff00 !important;
|
||||
}
|
||||
60
src/model.ts
Normal file
60
src/model.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
interface Fact {
|
||||
context?: string; // url of the post or profile, optional
|
||||
name: string;
|
||||
type: 'string' | 'number' | 'url' | 'boolean' | 'datetime' | 'timestr' | 'name' | 'md' | 'text' | 'integer';
|
||||
value: string | number | boolean | object;
|
||||
}
|
||||
|
||||
interface FactCollection {
|
||||
url: string;
|
||||
facts: Fact[];
|
||||
savedAt: string; // time.now()
|
||||
type: 'profile' | 'post' | 'search' |'comment';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param name property name
|
||||
* @param type property type
|
||||
* @param value property value
|
||||
* @param context optional context, usually the url of the post or profile
|
||||
* @returns
|
||||
*/
|
||||
|
||||
export function NewFact( name: string, type:'string' | 'number' | 'url' | 'boolean' | 'datetime' | 'timestr' | 'name' | 'md' | 'text' | 'integer', value:string, context?:string):Fact {
|
||||
const fact:Fact = {
|
||||
context: context,
|
||||
name: name,
|
||||
type:type,
|
||||
value: value,
|
||||
}
|
||||
|
||||
return fact;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param contextp url of the post or profile
|
||||
* @param facts array of facts
|
||||
* @param type type of the fact collection, can be 'profile', 'post', or 'comment'
|
||||
* @returns
|
||||
*/
|
||||
|
||||
export function NewFactCollection(type: 'profile' | 'post' | 'comment' | 'search'): FactCollection {
|
||||
const factCollection: FactCollection = {
|
||||
url: window.location.href, // use window location href if url is not provided
|
||||
facts: [],
|
||||
savedAt: new Date().toISOString(),
|
||||
type: type,
|
||||
}
|
||||
|
||||
return factCollection;
|
||||
}
|
||||
|
||||
|
||||
export type{
|
||||
Fact,
|
||||
FactCollection,
|
||||
};
|
||||
1025
src/modules/facebook.ts
Normal file
1025
src/modules/facebook.ts
Normal file
File diff suppressed because it is too large
Load Diff
763
src/modules/instagram.ts
Normal file
763
src/modules/instagram.ts
Normal file
@@ -0,0 +1,763 @@
|
||||
import * as model from '../model';
|
||||
import { turndownService } from '../utils/turndown';
|
||||
import { parseCount } from '../helper/parseCount';
|
||||
|
||||
const SELECTORS = {
|
||||
profile: {
|
||||
container: 'main header',
|
||||
avatar: 'img[alt*="profile picture" i], img[alt*="profile" i], header img[alt]',
|
||||
displayName: 'header section:nth-child(4) span[dir="auto"]',
|
||||
verifiedBadge: 'svg[aria-label="Verified"], img[alt="Verified"]',
|
||||
bio: 'main header section:nth-child(4) > div > span',
|
||||
followers: 'a[href*="/followers/"] span',
|
||||
following: 'a[href*="/following/"] span',
|
||||
},
|
||||
post: {
|
||||
container: 'article',
|
||||
username: 'a[href*="/"][href$="/"]',
|
||||
caption: 'h1[dir="auto"]',
|
||||
likeCount: 'a[href*="/liked_by/"] span[class*="html-span"]',
|
||||
// media: 'video[src], img[src*="instagram"]',
|
||||
mediaContainer: 'div[role="presentation"]',
|
||||
videoUrl: 'article > div > div:first-child video[src], article > div > div:first-child video[src] source',
|
||||
imageUrl: 'article > div > div:first-child img[src]',
|
||||
|
||||
timestamp: 'time[datetime]',
|
||||
profileImage: 'img[alt*="profile"]',
|
||||
profileUrl: 'a[href*="/"][href$="/"]'
|
||||
},
|
||||
comments: {
|
||||
container: 'article > div > div:nth-child(2) > div > div > div:nth-child(2) > div > ul > div:nth-child(3) > div > div, main[role="main"] hr + div > div > div:nth-child(2)',
|
||||
sections: ':scope > div',
|
||||
profile_url: 'a[href*="/"]',
|
||||
user_name: 'a[href*="/"] span[dir="auto"]',
|
||||
createdat_datetime: 'time[datetime]',
|
||||
profileimage_url: 'img[alt*="profile"]',
|
||||
comment_url: 'a[href*="/p/"]',
|
||||
like_count: 'div[role="button"] > span[dir="auto"], span[dir="ltr"] button > span',
|
||||
replie_count: 'div[role="button"] > div[class^="html-div"] > span[dir="auto"], li ul li button > span',
|
||||
content_md: 'div[class^="html-div"] > div > div[class^="html-div"] > div[class^="html-div"] > div[class^="html-div"] > div > div[class^="html-div"] > div:nth-of-type(2)',
|
||||
username: 'a[href*="/"][href$="/"]',
|
||||
text: 'span[dir="auto"]',
|
||||
timestamp: 'time[datetime]',
|
||||
profileImage: 'img[alt*="profile"]'
|
||||
}
|
||||
};
|
||||
|
||||
export default class InstagramModule {
|
||||
async saveData(): Promise<model.FactCollection[]> {
|
||||
const collections: model.FactCollection[] = [];
|
||||
|
||||
try {
|
||||
if (this.isProfilePage()) {
|
||||
const profile = this.extractProfile();
|
||||
if (profile) collections.push(profile);
|
||||
|
||||
const posts = this.extractPostsList();
|
||||
if (posts) collections.push(posts);
|
||||
} else if (this.isPostPage()) {
|
||||
const post = this.extractDetailPost();
|
||||
if (post) collections.push(post);
|
||||
|
||||
const comments = this.extractComments();
|
||||
if (comments) collections.push(comments);
|
||||
}
|
||||
else if(window.location.href.includes('/explore/') || window.location.href.includes('/search/')){
|
||||
const posts = this.extractPostsList();
|
||||
if (posts?.facts && posts.facts.length > 0) collections.push(posts);
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving Instagram data:", error);
|
||||
}
|
||||
|
||||
return collections;
|
||||
}
|
||||
|
||||
private isProfilePage(): boolean {
|
||||
return !window.location.pathname.includes('/p/') && !window.location.pathname.includes('/reel/') && !window.location.href.includes('/search/');
|
||||
}
|
||||
|
||||
private isPostPage(): boolean {
|
||||
return window.location.pathname.includes('/p/') || window.location.pathname.includes('/reel/');
|
||||
}
|
||||
|
||||
|
||||
|
||||
private extractProfile(): model.FactCollection | null {
|
||||
if (this.isSaved(document.querySelector(SELECTORS.profile.container) as HTMLElement)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const profile = model.NewFactCollection('profile');
|
||||
profile.url = this.getCleanURL();
|
||||
|
||||
if (profile.url) profile.facts.push(model.NewFact("ig_profile_source_url", "url", profile.url, window.location.href));
|
||||
|
||||
const displayName = document.querySelector(SELECTORS.profile.displayName)?.textContent?.trim();
|
||||
|
||||
if (displayName) profile.facts.push(model.NewFact("ig_profile_display_name", "name", displayName, window.location.href));
|
||||
// Username from URL
|
||||
const username = this.getUsername();
|
||||
if (username) profile.facts.push(model.NewFact("ig_profile_user_name", "name", username, window.location.href));
|
||||
|
||||
// Avatar
|
||||
const avatar = document.querySelector(SELECTORS.profile.avatar) as HTMLImageElement;
|
||||
if (avatar?.src) profile.facts.push(model.NewFact("ig_profile_profileimage_url", "url", avatar.src, window.location.href));
|
||||
|
||||
// Bio
|
||||
const bioEl = document.querySelector(SELECTORS.profile.bio) as HTMLElement;
|
||||
const bioLinkEl = document.querySelector('main header section:nth-child(4) > div > a') as HTMLAnchorElement;
|
||||
const bioTurndown = turndownService.turndown(bioEl?.innerHTML?.trim() || '');
|
||||
|
||||
|
||||
|
||||
if (bioEl) {
|
||||
profile.facts.push(model.NewFact("ig_profile_bio_text", "text", bioTurndown
|
||||
, window.location.href));
|
||||
}
|
||||
// Stats from meta description
|
||||
const stats = this.extractStatsFromMeta();
|
||||
Object.entries(stats).forEach(([key, value]) => {
|
||||
if (value) profile.facts.push(model.NewFact(key, "integer", String(this.parseNumber(value)), window.location.href));
|
||||
});
|
||||
|
||||
if (!stats.followerCount) {
|
||||
// Fallback to followers count from profile page
|
||||
const followersEl = document.querySelector(SELECTORS.profile.followers) as HTMLElement;
|
||||
if (followersEl) {
|
||||
const followerCount = followersEl.textContent?.trim();
|
||||
const cleanedfollowerCount = parseCount(followerCount.split(' ')[0]);
|
||||
|
||||
if (followerCount) {
|
||||
profile.facts.push(model.NewFact("ig_profile_follower_count", "integer", cleanedfollowerCount, window.location.href));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!stats.followerCount) {
|
||||
// Fallback to following count from profile page
|
||||
const followingEl = document.querySelector(SELECTORS.profile.following) as HTMLElement;
|
||||
if (followingEl) {
|
||||
const followingCount = followingEl.textContent?.trim();
|
||||
const cleanedFollowingCount = parseCount(followingCount.split(' ')[0]);
|
||||
|
||||
if (followingCount) {
|
||||
profile.facts.push(model.NewFact("ig_profile_following_count", "integer", cleanedFollowingCount, window.location.href));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (profile.facts.length > 0) {
|
||||
this.addClassSaved(document.querySelector(SELECTORS.profile.container) as HTMLElement);
|
||||
return profile;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extractPostsList(): model.FactCollection | null {
|
||||
const posts = window.location.href.includes('/search/') ? model.NewFactCollection('search') : model.NewFactCollection('post');
|
||||
posts.url = this.getCleanURL();
|
||||
|
||||
const postLinks = document.querySelectorAll<HTMLAnchorElement>('a[href*="/p/"]:not(.saved), a[href*="/reel/"]:not(.saved)');
|
||||
|
||||
postLinks.forEach(link => {
|
||||
posts.facts.push(model.NewFact("ig_post_source_url", "url", link.href, link.href));
|
||||
|
||||
// const img = link.querySelector('img') as HTMLImageElement;
|
||||
// if (img?.src) {
|
||||
// posts.facts.push(model.NewFact("thumbnail_url", "url", img.src, link.href));
|
||||
// if (img.alt) posts.facts.push(model.NewFact("description", "string", img.alt, link.href));
|
||||
// }
|
||||
|
||||
// Tandai sebagai tersimpan
|
||||
this.addClassSaved(link);
|
||||
});
|
||||
|
||||
return posts.facts.length > 0 ? posts : null;
|
||||
}
|
||||
private extractFromJSON(): model.FactCollection | null {
|
||||
const scripts = document.querySelectorAll('script[type="application/json"]:not(.saved)');
|
||||
const postId = this.getPostId();
|
||||
|
||||
for (const script of scripts) {
|
||||
const content = script.textContent;
|
||||
if (!content || !content.includes('like_count') || !content.includes(postId)) continue;
|
||||
|
||||
try {
|
||||
const data = JSON.parse(content);
|
||||
const postData = this.findPostInJSON(data);
|
||||
if (!postData) continue;
|
||||
|
||||
console.log('📦 Found post data structure:', {
|
||||
hasCarousel: !!postData.carousel_media,
|
||||
mediaType: postData.media_type,
|
||||
productType: postData.product_type,
|
||||
carouselCount: postData.carousel_media?.length || 0,
|
||||
hasUser: !!(postData.user || postData.owner),
|
||||
username: (postData.user || postData.owner)?.username
|
||||
});
|
||||
|
||||
const post = model.NewFactCollection('post');
|
||||
post.url = this.getCleanURL();
|
||||
|
||||
// Basic fields
|
||||
post.facts.push(model.NewFact("ig_post_source_url", "url", post.url, window.location.href));
|
||||
|
||||
// Extract user information
|
||||
const userInfo = postData.user || postData.owner;
|
||||
if (userInfo) {
|
||||
console.log('👤 Found user info:', {
|
||||
username: userInfo.username,
|
||||
hasProfilePic: !!userInfo.profile_pic_url,
|
||||
hasHDProfilePic: !!userInfo.hd_profile_pic_url_info?.url
|
||||
});
|
||||
|
||||
if (userInfo.username) {
|
||||
post.facts.push(model.NewFact("ig_post_user_name", "name", userInfo.username, window.location.href));
|
||||
|
||||
// Generate profile URL from username
|
||||
const profileUrl = `https://www.instagram.com/${userInfo.username}/`;
|
||||
post.facts.push(model.NewFact("ig_post_profile_url", "url", profileUrl, window.location.href));
|
||||
console.log('✅ Added username and profile URL');
|
||||
}
|
||||
|
||||
// Extract profile image
|
||||
if (userInfo.profile_pic_url) {
|
||||
post.facts.push(model.NewFact("ig_post_profileimage_url", "url", userInfo.profile_pic_url, window.location.href));
|
||||
console.log('✅ Added profile image from profile_pic_url');
|
||||
} else if (userInfo.hd_profile_pic_url_info?.url) {
|
||||
post.facts.push(model.NewFact("ig_post_profileimage_url", "url", userInfo.hd_profile_pic_url_info.url));
|
||||
console.log('✅ Added profile image from hd_profile_pic_url_info');
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ No user info found in post data');
|
||||
}
|
||||
|
||||
// Extract media content (images/videos) and caption
|
||||
const mediaContent = this.extractMediaFromJSON(postData, data);
|
||||
if (mediaContent.content) {
|
||||
post.facts.push(model.NewFact("ig_post_content_md", "md", mediaContent.content, window.location.href));
|
||||
console.log('✅ Extracted media content successfully');
|
||||
}
|
||||
|
||||
if (postData.like_count > 0) {
|
||||
post.facts.push(model.NewFact("ig_post_like_count", "integer", String(postData.like_count), window.location.href));
|
||||
}
|
||||
|
||||
if (postData.comment_count >= 0) {
|
||||
post.facts.push(model.NewFact("ig_post_comment_count", "integer", String(postData.comment_count), window.location.href));
|
||||
}
|
||||
|
||||
// // Legacy single media URL
|
||||
// if (postData.display_url) {
|
||||
// post.facts.push(model.NewFact("ig_post_media_url", "url", postData.display_url, window.location.href));
|
||||
// }
|
||||
|
||||
const date = document.querySelector('time[datetime]');
|
||||
if (date) {
|
||||
const datetime = date.getAttribute('datetime');
|
||||
if (datetime) {
|
||||
post.facts.push(model.NewFact("ig_post_createdat_datetime", "datetime", datetime, window.location.href));
|
||||
}
|
||||
}
|
||||
|
||||
this.addClassSaved(script as HTMLElement);
|
||||
return post.facts.length > 0 ? post : null;
|
||||
} catch (error) {
|
||||
console.error('❌ Error parsing JSON:', error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
private extractDetailPost(): model.FactCollection | null {
|
||||
const post = model.NewFactCollection('post');
|
||||
post.url = this.getCleanURL();
|
||||
|
||||
// Try JSON first, then DOM
|
||||
const jsonPost = this.extractFromJSON();
|
||||
if (jsonPost) {
|
||||
|
||||
return jsonPost;
|
||||
}
|
||||
|
||||
// Basic info
|
||||
post.facts.push(model.NewFact("ig_post_source_url", "url", window.location.href));
|
||||
|
||||
const container = document.querySelector(SELECTORS.post.container);
|
||||
if (!container || container.classList.contains('saved')) return null;
|
||||
|
||||
// Username
|
||||
const usernameEl = this.findElement(SELECTORS.post.username, container);
|
||||
if (usernameEl) {
|
||||
const username = this.extractUsernameFromHref(usernameEl.getAttribute('href'));
|
||||
if (username) post.facts.push(model.NewFact("ig_post_user_name", "name", username, window.location.href));
|
||||
}
|
||||
|
||||
// Profile URL
|
||||
const profileUrlEl = this.findElement(SELECTORS.post.profileUrl, container);
|
||||
if (profileUrlEl) {
|
||||
const profileUrl = profileUrlEl.getAttribute('href');
|
||||
const fullProfileUrl = profileUrl?.includes('instagram.com') ? profileUrl : `https://www.instagram.com${profileUrl}`;
|
||||
if (profileUrl) post.facts.push(model.NewFact("ig_post_profile_url", "url", fullProfileUrl, window.location.href));
|
||||
}
|
||||
|
||||
// Avatar
|
||||
const profileImage = this.findElement(SELECTORS.post.profileImage, container) as HTMLImageElement;
|
||||
if (profileImage?.src) {
|
||||
|
||||
post.facts.push(model.NewFact("ig_post_profileimage_url", "url", profileImage.src, window.location.href));
|
||||
}
|
||||
|
||||
// Content of post
|
||||
const caption = this.findElement(SELECTORS.post.caption, container)?.textContent?.trim();
|
||||
|
||||
const mediaContainer = this.findElement(SELECTORS.post.mediaContainer, container);
|
||||
|
||||
const imageUrl = mediaContainer?.querySelectorAll(SELECTORS.post.imageUrl) as NodeListOf<HTMLImageElement>;
|
||||
|
||||
const imageUrls: string[] = [...imageUrl].map(img => img.src).filter(src => src);
|
||||
|
||||
|
||||
const videoUrl = mediaContainer?.querySelectorAll(SELECTORS.post.videoUrl) as NodeListOf<HTMLVideoElement>;
|
||||
|
||||
const videoUrls: string[] = [...videoUrl].map(video => video.src).filter(src => src);
|
||||
|
||||
const contentParts: string[] = [];
|
||||
|
||||
// Add images
|
||||
if (imageUrls.length > 0) {
|
||||
const imageMarkdown = imageUrls.map(url => ``).join('\n');
|
||||
contentParts.push(imageMarkdown);
|
||||
}
|
||||
|
||||
// Add video
|
||||
if (videoUrls.length > 0) {
|
||||
const videoMarkdown = videoUrls.map(url => ``).join('\n');
|
||||
contentParts.push(videoMarkdown);
|
||||
}
|
||||
// Add caption
|
||||
if (caption) {
|
||||
contentParts.push(caption);
|
||||
}
|
||||
|
||||
// Join dengan double newline untuk proper spacing
|
||||
const contentTurndown = contentParts.join('\n\n');
|
||||
|
||||
post.facts.push(model.NewFact("ig_content_md", "md", contentTurndown, window.location.href));
|
||||
// Likes
|
||||
const likesEl = this.findElement(SELECTORS.post.likeCount, container);
|
||||
if (likesEl?.textContent) {
|
||||
const likes = this.parseNumber(likesEl.textContent);
|
||||
if (likes > 0) post.facts.push(model.NewFact("ig_post_like_count", "integer", String(likes), window.location.href));
|
||||
}
|
||||
// Timestamp
|
||||
const timeEl = this.findElement(SELECTORS.post.timestamp, container);
|
||||
if (timeEl) {
|
||||
const datetime = timeEl.getAttribute('datetime');
|
||||
if (datetime) post.facts.push(model.NewFact("ig_post_createdat_datetime", "datetime", datetime, window.location.href));
|
||||
}
|
||||
|
||||
// Tandai container sebagai tersimpan
|
||||
if (post.facts.length > 0) {
|
||||
this.addClassSaved(container as HTMLElement);
|
||||
}
|
||||
|
||||
return post.facts.length > 0 ? post : null;
|
||||
}
|
||||
|
||||
// Extract post data from JSON embedded in the page (example: ../../htmlexample/instagram/detailPost.json)
|
||||
|
||||
|
||||
private findPostInJSON(obj: any): any {
|
||||
if (!obj || typeof obj !== 'object') return null;
|
||||
|
||||
// Check if current object is a valid post
|
||||
if (obj.like_count !== undefined && (obj.user || obj.owner)) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
// Check for carousel container (product_type: "carousel_container")
|
||||
if (obj.product_type === 'carousel_container' && obj.carousel_media) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
// Search in common paths
|
||||
const paths = [
|
||||
'graphql.shortcode_media',
|
||||
'media',
|
||||
'__bbox.result',
|
||||
'xdt_api__v1__media__shortcode__web_info.items.0', // New Instagram API format
|
||||
'require.0.3.0.__bbox.require.0.3.1.__bbox.result.data.xdt_api__v1__media__shortcode__web_info.items.0'
|
||||
];
|
||||
|
||||
for (const path of paths) {
|
||||
const result = this.getNestedValue(obj, path);
|
||||
if (result && (result.like_count !== undefined || result.carousel_media)) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Special handling for nested carousel structure
|
||||
if (obj.items && Array.isArray(obj.items) && obj.items.length > 0) {
|
||||
const firstItem = obj.items[0];
|
||||
if (firstItem.carousel_media || firstItem.like_count !== undefined) {
|
||||
return firstItem;
|
||||
}
|
||||
}
|
||||
|
||||
// Recursive search with depth limit to avoid infinite loops
|
||||
return this.findPostInJSONRecursive(obj, 0, 5);
|
||||
}
|
||||
|
||||
private findPostInJSONRecursive(obj: any, depth: number, maxDepth: number): any {
|
||||
if (depth >= maxDepth || !obj || typeof obj !== 'object') return null;
|
||||
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
const value = obj[key];
|
||||
|
||||
// Check current value
|
||||
if (value && typeof value === 'object') {
|
||||
if (value.like_count !== undefined && (value.user || value.owner)) {
|
||||
return value;
|
||||
}
|
||||
if (value.product_type === 'carousel_container' && value.carousel_media) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Recursive search
|
||||
const result = this.findPostInJSONRecursive(value, depth + 1, maxDepth);
|
||||
if (result) return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private extractMediaFromJSON(postData: any, fullData: any): { content: string } {
|
||||
const contentParts: string[] = [];
|
||||
const imageUrls: string[] = [];
|
||||
const videoUrls: string[] = [];
|
||||
|
||||
console.log('🔍 Extracting media from post data...');
|
||||
|
||||
// Check if it's a carousel post
|
||||
if (postData.carousel_media && Array.isArray(postData.carousel_media)) {
|
||||
console.log(`📸 Found carousel with ${postData.carousel_media.length} items`);
|
||||
|
||||
// Extract media from carousel
|
||||
for (const mediaItem of postData.carousel_media) {
|
||||
if (mediaItem.media_type === 2 && mediaItem.video_versions) {
|
||||
// Video media
|
||||
const videoUrl = this.getBestVideoURL(mediaItem.video_versions);
|
||||
if (videoUrl) {
|
||||
videoUrls.push(videoUrl);
|
||||
console.log('🎥 Added video URL from carousel');
|
||||
}
|
||||
} else if (mediaItem.media_type === 1 && mediaItem.image_versions2?.candidates) {
|
||||
// Image media
|
||||
const imageUrl = this.getBestImageURL(mediaItem.image_versions2.candidates);
|
||||
if (imageUrl) {
|
||||
imageUrls.push(imageUrl);
|
||||
console.log('📷 Added image URL from carousel');
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('📱 Processing single media post');
|
||||
|
||||
// Single media post
|
||||
if (postData.media_type === 2 && postData.video_versions) {
|
||||
// Single video
|
||||
const videoUrl = this.getBestVideoURL(postData.video_versions);
|
||||
if (videoUrl) {
|
||||
videoUrls.push(videoUrl);
|
||||
// console.log('🎥 Added single video URL');
|
||||
}
|
||||
} else if (postData.media_type === 1 && postData.image_versions2?.candidates) {
|
||||
// Single image
|
||||
const imageUrl = this.getBestImageURL(postData.image_versions2.candidates);
|
||||
if (imageUrl) {
|
||||
imageUrls.push(imageUrl);
|
||||
// console.log('📷 Added single image URL');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for video_dash_prefetch_representations (from multiplepost.json structure)
|
||||
const extensions = this.findExtensionsInJSON(fullData);
|
||||
if (extensions?.all_video_dash_prefetch_representations) {
|
||||
console.log(`🎬 Found ${extensions.all_video_dash_prefetch_representations.length} video dash representations`);
|
||||
|
||||
for (const videoData of extensions.all_video_dash_prefetch_representations) {
|
||||
if (videoData.representations) {
|
||||
for (const rep of videoData.representations) {
|
||||
if (rep.mime_type === 'video/mp4' && rep.base_url) {
|
||||
videoUrls.push(rep.base_url);
|
||||
// console.log('🎥 Added video URL from dash representation');
|
||||
break; // Take first video URL found per video
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`📊 Media extraction result: ${imageUrls.length} images, ${videoUrls.length} videos`);
|
||||
|
||||
// Build content in Markdown format
|
||||
if (imageUrls.length > 0) {
|
||||
const imageMarkdown = imageUrls.map(url => ``).join('\n');
|
||||
contentParts.push(imageMarkdown);
|
||||
}
|
||||
|
||||
if (videoUrls.length > 0) {
|
||||
const videoMarkdown = videoUrls.map(url => ``).join('\n');
|
||||
contentParts.push(videoMarkdown);
|
||||
}
|
||||
|
||||
// Add caption if exists
|
||||
if (postData.caption?.text) {
|
||||
contentParts.push(postData.caption.text);
|
||||
console.log('📝 Added caption text');
|
||||
}
|
||||
|
||||
return {
|
||||
content: contentParts.join('\n\n')
|
||||
};
|
||||
}
|
||||
|
||||
private findExtensionsInJSON(obj: any): any {
|
||||
if (!obj || typeof obj !== 'object') return null;
|
||||
|
||||
// Direct extensions property
|
||||
if (obj.extensions) return obj.extensions;
|
||||
|
||||
// Search recursively with limited depth
|
||||
const searchExtensions = (data: any, depth: number = 0): any => {
|
||||
if (depth > 3 || !data || typeof data !== 'object') return null;
|
||||
|
||||
for (const key in data) {
|
||||
if (data.hasOwnProperty(key)) {
|
||||
if (key === 'extensions' && data[key]) return data[key];
|
||||
|
||||
if (typeof data[key] === 'object') {
|
||||
const nested = searchExtensions(data[key], depth + 1);
|
||||
if (nested) return nested;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return searchExtensions(obj);
|
||||
}
|
||||
|
||||
private getBestVideoURL(videoVersions: any[]): string | null {
|
||||
if (!videoVersions || videoVersions.length === 0) return null;
|
||||
|
||||
// Find highest quality video (type 101 is usually best quality)
|
||||
const bestVideo = videoVersions.find(v => v.type === 101) || videoVersions[0];
|
||||
return bestVideo?.url || null;
|
||||
}
|
||||
|
||||
private getBestImageURL(candidates: any[]): string | null {
|
||||
if (!candidates || candidates.length === 0) return null;
|
||||
|
||||
// Find highest resolution image
|
||||
const validCandidates = candidates.filter(c => c.url && c.width && c.height);
|
||||
if (validCandidates.length === 0) return null;
|
||||
|
||||
const bestImage = validCandidates.reduce((best, current) => {
|
||||
const currentRes = current.width * current.height;
|
||||
const bestRes = best.width * best.height;
|
||||
return currentRes > bestRes ? current : best;
|
||||
}, validCandidates[0]);
|
||||
|
||||
return bestImage?.url || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts **all** top-level comments from an Instagram post
|
||||
* using the exact DOM path proven in the JSDOM demo.
|
||||
*/
|
||||
private extractComments(): model.FactCollection | null {
|
||||
const container = document.querySelector(
|
||||
SELECTORS.comments.container
|
||||
) as HTMLElement | null;
|
||||
|
||||
if (!container) return null;
|
||||
|
||||
/* 👉 grab every top-level comment block */
|
||||
const sections = container.querySelectorAll<HTMLElement>(':scope > div');
|
||||
if (sections.length === 0) return null;
|
||||
|
||||
const facts = model.NewFactCollection('comment');
|
||||
|
||||
sections.forEach(section => {
|
||||
if (section.classList.contains('saved')) return;
|
||||
|
||||
const profile_url = section.querySelector(SELECTORS.comments.profile_url);
|
||||
const user_name = section.querySelector(SELECTORS.comments.user_name);
|
||||
const createdat_datetime = section.querySelector(SELECTORS.comments.createdat_datetime);
|
||||
const profileimage_url = section.querySelector(SELECTORS.comments.profileimage_url);
|
||||
const comment_url = section.querySelector(SELECTORS.comments.comment_url);
|
||||
const like_count = section.querySelector(SELECTORS.comments.like_count);
|
||||
const replie_count = section.querySelector(SELECTORS.comments.replie_count);
|
||||
const content_md = section.querySelector(SELECTORS.comments.content_md);
|
||||
|
||||
const commentUrl = comment_url?.getAttribute('href');
|
||||
|
||||
const fullCommentUrl = commentUrl ? new URL(commentUrl, window.location.href).toString() : null;
|
||||
|
||||
if (fullCommentUrl) {
|
||||
if (commentUrl) facts.facts.push(model.NewFact("ig_comment_source_url", "url", fullCommentUrl, window.location.href));
|
||||
}
|
||||
|
||||
if (profile_url) {
|
||||
const profileUrl = profile_url.getAttribute('href')
|
||||
const fullProfileUrl = profileUrl?.includes('instagram.com') ? profileUrl : `https://www.instagram.com${profileUrl}`;
|
||||
if (profileUrl) facts.facts.push(model.NewFact("ig_comment_profile_url", "name", fullProfileUrl, fullCommentUrl ?? window.location.href));
|
||||
|
||||
const username = this.extractUsernameFromHref(profile_url.getAttribute('href'));
|
||||
if (username) facts.facts.push(model.NewFact("ig_comment_user_name", "name", username, fullCommentUrl ?? window.location.href));
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (content_md) {
|
||||
const contentTurndown = turndownService.turndown(content_md.innerHTML);
|
||||
if (contentTurndown) facts.facts.push(model.NewFact("ig_comment_content_md", "md", contentTurndown, fullCommentUrl ?? window.location.href));
|
||||
}
|
||||
|
||||
if (profileimage_url) {
|
||||
const imageUrl = (profileimage_url as HTMLImageElement).src;
|
||||
if (imageUrl) facts.facts.push(model.NewFact("ig_comment_profileimage_url", "url", imageUrl, fullCommentUrl ?? window.location.href));
|
||||
}
|
||||
|
||||
if (like_count) {
|
||||
const likes = like_count.textContent?.trim() || '0';
|
||||
if (likes) {
|
||||
const likeMatch = likes.replaceAll(',', '').match(/\d+/g);
|
||||
const likeCountStr = likeMatch ? likeMatch.join('') : '0';
|
||||
facts.facts.push(model.NewFact("ig_comment_like_count", "integer", likeCountStr, fullCommentUrl ?? window.location.href));
|
||||
}
|
||||
}
|
||||
|
||||
if (replie_count) {
|
||||
const replies = replie_count.textContent?.trim() || '0';
|
||||
if (replies) {
|
||||
const replyMatch = replies.replaceAll(',', '').match(/\d+/g);
|
||||
const replyCountStr = replyMatch ? replyMatch.join('') : '0';
|
||||
facts.facts.push(model.NewFact("ig_comment_comment_count", "integer", replyCountStr, fullCommentUrl ?? window.location.href));
|
||||
}
|
||||
}
|
||||
|
||||
if (createdat_datetime) {
|
||||
const datetime = createdat_datetime.getAttribute('datetime');
|
||||
if (datetime) facts.facts.push(model.NewFact("ig_comment_createdat_datetime", "datetime", datetime, fullCommentUrl ?? window.location.href));
|
||||
}
|
||||
|
||||
this.addClassSaved(section as HTMLElement); // Mark section as saved
|
||||
});
|
||||
|
||||
// container.classList.add('saved');
|
||||
return facts.facts.length > 0 ? facts : null;
|
||||
}
|
||||
|
||||
|
||||
private getCleanURL(): string {
|
||||
const url = new URL(window.location.href);
|
||||
// Remove img_index parameter
|
||||
url.searchParams.delete('img_index');
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
private getUsername(): string | null {
|
||||
const segments = window.location.pathname.split('/').filter(Boolean);
|
||||
return segments[0] && segments[0].length < 50 ? segments[0] : null;
|
||||
}
|
||||
|
||||
private extractStatsFromMeta(): Record<string, string> {
|
||||
const ogDesc = document.querySelector('meta[property="og:description"]');
|
||||
const content = ogDesc?.getAttribute('content') || '';
|
||||
|
||||
const stats: Record<string, string> = {};
|
||||
|
||||
const patterns = {
|
||||
ig_profile_post_count: /(\d+[,\d]*)\s*Posts/i,
|
||||
ig_profile_follower_count: /(\d+[,\d]*)\s*Followers/i,
|
||||
ig_profile_following_count: /(\d+[,\d]*)\s*Following/i
|
||||
};
|
||||
|
||||
Object.entries(patterns).forEach(([key, pattern]) => {
|
||||
const match = content.match(pattern);
|
||||
if (match) stats[key] = match[1];
|
||||
});
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
private extractUsernameFromHref(href: string | null): string | null {
|
||||
if (!href) return null;
|
||||
const match = href.match(/\/([^\/]+)\/$/);
|
||||
return match && match[1] !== 'p' && match[1] !== 'reel' ? match[1] : null;
|
||||
}
|
||||
|
||||
private findElement(selector: string, container?: Element | null): Element | null {
|
||||
return container ? container.querySelector(selector) : document.querySelector(selector);
|
||||
}
|
||||
|
||||
private parseNumber(text: string): number {
|
||||
const clean = text.toLowerCase().replace(/[^0-9km.,]/g, '');
|
||||
if (clean.includes('k')) return Math.round(parseFloat(clean.replace('k', '')) * 1000);
|
||||
if (clean.includes('m')) return Math.round(parseFloat(clean.replace('m', '')) * 1000000);
|
||||
return parseInt(clean.replace(/[,.]/g, '')) || 0;
|
||||
}
|
||||
|
||||
private getNestedValue(obj: any, path: string): any {
|
||||
return path.split('.').reduce((current, key) => current?.[key], obj);
|
||||
}
|
||||
|
||||
private isTimeElement(text: string): boolean {
|
||||
return /^\d+[dwmh]$/.test(text);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private getPostId(): string {
|
||||
const match = window.location.pathname.match(/\/p\/([^/]+)/);
|
||||
return match ? match[1] : '';
|
||||
}
|
||||
|
||||
tagElement(): void {
|
||||
const elements = document.querySelectorAll('article, img[alt*="profile"], a[href*="/p/"], a[href*="/reel/"]');
|
||||
elements.forEach(el => el.classList.add('tagged'));
|
||||
console.log(window.location.hostname);
|
||||
|
||||
|
||||
}
|
||||
|
||||
addClassTagged(element: HTMLElement): void {
|
||||
element.classList.add('tagged');
|
||||
}
|
||||
|
||||
addClassSaved(element: HTMLElement): void {
|
||||
element.classList.add('saved');
|
||||
}
|
||||
|
||||
isTagged(element: HTMLElement): boolean {
|
||||
return element.classList.contains('tagged');
|
||||
}
|
||||
|
||||
isSaved(element: HTMLElement): boolean {
|
||||
return element.classList.contains('saved');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
629
src/modules/linkedin.ts
Normal file
629
src/modules/linkedin.ts
Normal file
@@ -0,0 +1,629 @@
|
||||
import * as model from "../model";
|
||||
import { NewFact } from "../model";
|
||||
import { turndownService } from "../utils/turndown";
|
||||
|
||||
const ELEMENT_SELECTORS = {
|
||||
profile: {
|
||||
container: "#recent-activity-top-card",
|
||||
displayName: "h1[class*='org-top-card-summary__title'], #recent-activity-top-card h3",
|
||||
description: "#recent-activity-top-card h4",
|
||||
image: "[class*='logo-container'] img, img.pv-recent-activity-top-card__member-photo",
|
||||
followerCount: "li[class='text-body-small t-black--light inline-block'] > span, div.pv-recent-activity-top-card__extra-info > div > div:nth-child(2)",
|
||||
},
|
||||
post: {
|
||||
container: "[data-view-name='feed-full-update']",
|
||||
showMore: "button[class*='text__see-more'], button[role='button'][class*='see-more']",
|
||||
url: "div.feed-shared-update-v2",
|
||||
displayName: "span[class='update-components-actor__title'] span[dir='ltr'] > span > span, span[class='update-components-actor__title'] span[dir='ltr'] > span:first-of-type",
|
||||
username: "span.feed-shared-actor__sub-description a",
|
||||
profileImage: "div[class*='ivm-view-attr__img-wrapper'] img",
|
||||
content: [
|
||||
"div.feed-shared-inline-show-more-text",
|
||||
"[class*='update-components-image__container-wrapper'] img",
|
||||
"[class*='video-js media-player__player'] video",
|
||||
"div.ivm-view-attr__blur-background img",
|
||||
],
|
||||
like: ["button[aria-label*='reactions'] span", "button.social-details-social-counts__count-value span"],
|
||||
comment: "li[class*='social-details-social-counts__comments'] button",
|
||||
share: "button[class*='social-details-social-counts__item--truncate-text'] > span",
|
||||
date: '.update-components-actor__sub-description span[aria-hidden="true"]'
|
||||
},
|
||||
comment: {
|
||||
container: "article.comments-comment-entity",
|
||||
displayName: "span.comments-comment-meta__description-title",
|
||||
urlProfile: "article.comments-comment-entity a",
|
||||
profileImage: "div[class*='ivm-view-attr__img-wrapper'] img",
|
||||
content: [
|
||||
"div.update-components-text",
|
||||
"[class*='update-components-image__container-wrapper'] img",
|
||||
"[class*='video-js media-player__player'] video",
|
||||
],
|
||||
like: "button.comments-comment-social-bar__reactions-count--cr > span",
|
||||
commentCount: "span.comments-comment-social-bar__replies-count--cr",
|
||||
date: "time.comments-comment-meta__data"
|
||||
}
|
||||
} as const;
|
||||
|
||||
export default class LinkedInModule {
|
||||
context: string = "";
|
||||
followerCount: string = "";
|
||||
private currentUrl: string = "";
|
||||
|
||||
constructor() {
|
||||
this.currentUrl = window.location.href;
|
||||
this.updateFollowerCount();
|
||||
|
||||
// Reset follower count when URL changes (different profile)
|
||||
window.addEventListener('popstate', () => this.handleUrlChange());
|
||||
|
||||
// Monitor URL changes for SPA navigation
|
||||
setInterval(() => {
|
||||
if (window.location.href !== this.currentUrl) {
|
||||
this.handleUrlChange();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Continue monitoring follower count
|
||||
setInterval(() => this.updateFollowerCount(), 2000);
|
||||
}
|
||||
|
||||
private handleUrlChange(): void {
|
||||
const newUrl = window.location.href;
|
||||
if (newUrl !== this.currentUrl) {
|
||||
console.log('LinkedIn URL changed from', this.currentUrl, 'to', newUrl);
|
||||
this.currentUrl = newUrl;
|
||||
|
||||
// Reset follower count for new profile
|
||||
this.followerCount = "";
|
||||
|
||||
// Start monitoring follower count for new page
|
||||
this.updateFollowerCount();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private updateFollowerCount() {
|
||||
const followerElement = document.querySelector<HTMLElement>(ELEMENT_SELECTORS.profile.followerCount);
|
||||
|
||||
|
||||
if (followerElement && followerElement.textContent && this.followerCount == "") {
|
||||
const newCount = followerElement.textContent.trim();
|
||||
if (newCount !== this.followerCount) {
|
||||
this.followerCount = newCount;
|
||||
console.log("Follower count updated:", this.followerCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tagElement(): void {
|
||||
Object.keys(ELEMENT_SELECTORS).forEach((type) => {
|
||||
const selectorObj = ELEMENT_SELECTORS[type as keyof typeof ELEMENT_SELECTORS];
|
||||
Object.entries(selectorObj).forEach(([key, value]) => {
|
||||
if (key === "showMore" && typeof value === 'string') {
|
||||
const showMoreBtns = document.querySelectorAll<HTMLElement>(value);
|
||||
showMoreBtns.forEach(btn => {
|
||||
try {
|
||||
// Check if button is visible and clickable before clicking
|
||||
if (btn && btn.offsetParent !== null && !(btn as HTMLButtonElement).disabled) {
|
||||
btn.click();
|
||||
console.log("Clicked show more button in tagElement");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error clicking show more button in tagElement:", e);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(sel => {
|
||||
document.querySelectorAll<HTMLElement>(sel).forEach(element => this.addClassTagged(element));
|
||||
});
|
||||
} else {
|
||||
document.querySelectorAll<HTMLElement>(value).forEach(element => this.addClassTagged(element));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async saveData(): Promise<model.FactCollection[]> {
|
||||
const factCollections: model.FactCollection[] = [];
|
||||
|
||||
try {
|
||||
if (this.isProfilePage()) {
|
||||
await this.handleProfilePage(factCollections);
|
||||
} else if (this.isPostPage()) {
|
||||
await this.handlePostPage(factCollections);
|
||||
}else if(this.isSearchPage()){
|
||||
await this.handleProfilePage(factCollections);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in saveData:", error);
|
||||
}
|
||||
|
||||
return factCollections;
|
||||
}
|
||||
|
||||
private isProfilePage(): boolean {
|
||||
return window.location.href.includes('/company/') || window.location.href.includes('/in/');
|
||||
}
|
||||
|
||||
private isPostPage(): boolean {
|
||||
return window.location.href.includes('/posts/') || window.location.href.includes('/feed/update/');
|
||||
}
|
||||
|
||||
private isSearchPage(): boolean {
|
||||
return window.location.href.includes('/search/results/');
|
||||
}
|
||||
|
||||
private async handleProfilePage(factCollections: model.FactCollection[]): Promise<void> {
|
||||
console.log("Extracting profile data...");
|
||||
const profile = this.saveProfile();
|
||||
if (profile.facts.length > 0) {
|
||||
|
||||
factCollections.push(profile);
|
||||
this.markElementSaved(ELEMENT_SELECTORS.profile.container);
|
||||
console.log("Profile data extracted successfully");
|
||||
}
|
||||
|
||||
console.log("Extracting posts data...");
|
||||
const posts = await this.savePost();
|
||||
if (posts.facts.length > 0) {
|
||||
factCollections.push(posts);
|
||||
this.markElementSaved(ELEMENT_SELECTORS.post.container);
|
||||
console.log(`${posts.facts.length} post facts extracted successfully`);
|
||||
} else {
|
||||
console.log("No posts found or extraction failed");
|
||||
}
|
||||
}
|
||||
|
||||
private async handlePostPage(factCollections: model.FactCollection[]): Promise<void> {
|
||||
console.log("Extracting post data...");
|
||||
const posts = await this.savePost();
|
||||
if (posts.facts.length > 0) {
|
||||
factCollections.push(posts);
|
||||
this.markElementSaved(ELEMENT_SELECTORS.post.container);
|
||||
console.log(`${posts.facts.length} post facts extracted successfully`);
|
||||
}
|
||||
|
||||
console.log("Extracting comments data...");
|
||||
const comments = this.saveComment();
|
||||
if (comments.facts.length > 0) {
|
||||
factCollections.push(comments);
|
||||
this.markElementSaved(ELEMENT_SELECTORS.comment.container);
|
||||
console.log(`${comments.facts.length} comment facts extracted successfully`);
|
||||
}
|
||||
}
|
||||
|
||||
private markElementSaved(selector: string): void {
|
||||
const element = document.querySelector<HTMLElement>(selector) || document.body;
|
||||
this.addClassSaved(element);
|
||||
}
|
||||
|
||||
saveProfile(): model.FactCollection {
|
||||
const profileContainer = document.querySelector<HTMLElement>(
|
||||
ELEMENT_SELECTORS.profile.container
|
||||
);
|
||||
|
||||
/* 1. Skip if already saved */
|
||||
if (profileContainer && this.isSaved(profileContainer)) {
|
||||
const profileData = model.NewFactCollection('profile');
|
||||
return profileData;
|
||||
}
|
||||
|
||||
/* 2. Build the fact collection */
|
||||
const cleanedUrl = window.location.href.replaceAll('/recent-activity/all/', '');
|
||||
const profile: model.FactCollection = {
|
||||
url: window.location.href,
|
||||
facts: [],
|
||||
savedAt: new Date().toISOString(),
|
||||
type: 'profile',
|
||||
};
|
||||
|
||||
const elements = ELEMENT_SELECTORS.profile;
|
||||
const profileUrl = model.NewFact('li_profile_source_url', 'url', cleanedUrl, cleanedUrl);
|
||||
|
||||
// Get display name directly
|
||||
const displayNameElement = document.querySelector<HTMLElement>(elements.displayName);
|
||||
const displayName = displayNameElement?.textContent?.trim() || '';
|
||||
const displayNameFact = NewFact('li_profile_display_name', 'name', displayName, cleanedUrl);
|
||||
|
||||
// Get description directly
|
||||
const descriptionElement = document.querySelector<HTMLElement>(elements.description);
|
||||
const description = descriptionElement?.textContent?.trim() || '';
|
||||
const descriptionFact = NewFact('li_profile_bio_text', 'text', description, cleanedUrl);
|
||||
|
||||
if (
|
||||
displayNameFact?.value &&
|
||||
descriptionFact?.value
|
||||
) {
|
||||
// Get profile image directly
|
||||
const imgElement = document.querySelector<HTMLImageElement>(elements.image);
|
||||
const profileImageSrc = imgElement?.getAttribute('src') || '';
|
||||
const profileImageFact = NewFact('li_profile_profileimage_url', 'url', profileImageSrc, cleanedUrl);
|
||||
|
||||
// Get follower count directly
|
||||
let followerCountFact;
|
||||
if (!this.followerCount || this.followerCount.trim() === '') {
|
||||
followerCountFact = NewFact('li_profile_follower_count', 'number', '0', cleanedUrl);
|
||||
} else {
|
||||
const cleanedText = this.followerCount.trim().replace(/[^\d.,]/g, '');
|
||||
const numericValue = cleanedText.replace(/[.,]/g, '');
|
||||
const followerCount = Number(numericValue) || 0;
|
||||
followerCountFact = NewFact('li_profile_follower_count', 'number', followerCount.toString(), cleanedUrl);
|
||||
}
|
||||
|
||||
const profileFacts: model.Fact[] = [
|
||||
profileUrl,
|
||||
displayNameFact,
|
||||
descriptionFact,
|
||||
profileImageFact,
|
||||
followerCountFact,
|
||||
];
|
||||
profile.facts.push(...profileFacts);
|
||||
|
||||
/* 3. Mark container as saved only after success */
|
||||
if (profileContainer) {
|
||||
this.addClassSaved(profileContainer);
|
||||
}
|
||||
console.log('Profile data extracted successfully');
|
||||
} else {
|
||||
console.log('Profile extraction skipped – missing required fields');
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
async savePost(): Promise<model.FactCollection> {
|
||||
const posts: model.FactCollection = window.location.href.includes('/search/') ? model.NewFactCollection('search') : model.NewFactCollection('post');
|
||||
const selectors = ELEMENT_SELECTORS.post;
|
||||
const postElements = document.querySelectorAll<HTMLElement>(`${selectors.container}:not(.saved)`);
|
||||
|
||||
// First, click all show more buttons and wait for content to load
|
||||
await this.expandAllPostContent(postElements, selectors.showMore);
|
||||
|
||||
postElements.forEach((element) => {
|
||||
const postFacts = this.extractPostFromElement(element, selectors);
|
||||
posts.facts.push(...postFacts);
|
||||
this.addClassSaved(element);
|
||||
});
|
||||
|
||||
console.log(`Total post facts extracted: ${posts.facts.length}`);
|
||||
return posts;
|
||||
}
|
||||
|
||||
private async expandAllPostContent(postElements: NodeListOf<HTMLElement>, showMoreSelector: string): Promise<void> {
|
||||
const clickPromises: Promise<void>[] = [];
|
||||
|
||||
postElements.forEach((element) => {
|
||||
const btnShowMore = element.querySelectorAll<HTMLElement>(showMoreSelector);
|
||||
|
||||
btnShowMore.forEach(btn => {
|
||||
// Check if button is visible and clickable
|
||||
if (btn && btn.offsetParent !== null && !(btn as HTMLButtonElement).disabled) {
|
||||
clickPromises.push(this.clickShowMoreButton(btn));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Wait for all buttons to be clicked
|
||||
await Promise.all(clickPromises);
|
||||
|
||||
// Additional wait to allow content to fully load after clicking
|
||||
if (clickPromises.length > 0) {
|
||||
await this.delay(500); // Wait 500ms for content to load
|
||||
console.log(`Clicked ${clickPromises.length} show more buttons and waited for content to load`);
|
||||
}
|
||||
}
|
||||
|
||||
private async clickShowMoreButton(button: HTMLElement): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
try {
|
||||
button.click();
|
||||
// Small delay after each click to prevent overwhelming the page
|
||||
setTimeout(resolve, 100);
|
||||
} catch (e) {
|
||||
console.error("Error clicking show more button:", e);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private delay(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
saveComment(): model.FactCollection {
|
||||
const comments: model.FactCollection = model.NewFactCollection('comment');
|
||||
const selectors = ELEMENT_SELECTORS.comment;
|
||||
const commentElements = document.querySelectorAll<HTMLElement>(`${selectors.container}:not(.saved)`);
|
||||
|
||||
commentElements.forEach(element => this.addClassTagged(element));
|
||||
|
||||
commentElements.forEach((element) => {
|
||||
const commentFacts = this.extractCommentFromElement(element, selectors);
|
||||
comments.facts.push(...commentFacts);
|
||||
this.addClassSaved(element);
|
||||
});
|
||||
|
||||
console.log(`Total comment facts extracted: ${comments.facts.length}`);
|
||||
return comments;
|
||||
}
|
||||
|
||||
addClassTagged(element: HTMLElement): void {
|
||||
element.classList.add('tagged');
|
||||
}
|
||||
|
||||
addClassSaved(element: HTMLElement): void {
|
||||
element.classList.add('saved');
|
||||
}
|
||||
|
||||
private extractPostFromElement(element: HTMLElement, selectors: typeof ELEMENT_SELECTORS.post): model.Fact[] {
|
||||
const facts: model.Fact[] = [];
|
||||
const urnEl = element.querySelector<HTMLElement>(selectors.url);
|
||||
|
||||
|
||||
const urn = urnEl?.getAttribute('data-urn') || '';
|
||||
const contextUrl = `https://www.linkedin.com/feed/update/${urn}`;
|
||||
|
||||
if (urn) {
|
||||
facts.push(NewFact('li_post_source_url', 'url', contextUrl, contextUrl));
|
||||
}
|
||||
|
||||
const displayNameEl = element.querySelector<HTMLElement>(selectors.displayName);
|
||||
if (displayNameEl?.textContent?.trim()) {
|
||||
facts.push(NewFact('li_post_display_name', 'name', displayNameEl.textContent.trim(), contextUrl));
|
||||
}
|
||||
|
||||
const usernameEl = element.querySelector<HTMLElement>(selectors.username);
|
||||
if (usernameEl?.textContent?.trim()) {
|
||||
facts.push(NewFact('li_post_user_name', 'name', usernameEl.textContent.trim(), contextUrl));
|
||||
}
|
||||
|
||||
const profileImageEl = element.querySelector<HTMLElement>(selectors.profileImage);
|
||||
if (profileImageEl?.getAttribute('src')) {
|
||||
const profileImageUrl = profileImageEl.getAttribute('src') || '';
|
||||
facts.push(NewFact('li_post_profileimage_url', 'url', profileImageUrl, contextUrl));
|
||||
}
|
||||
|
||||
const content = this.extractContent(element, [...selectors.content]);
|
||||
if (content) {
|
||||
facts.push(NewFact('li_post_content_md', 'md', content, contextUrl));
|
||||
}
|
||||
|
||||
const likeCount = this.extractLikeCount(element, [...selectors.like]);
|
||||
if (likeCount) {
|
||||
facts.push(NewFact('li_post_like_count', 'number', likeCount, contextUrl));
|
||||
}
|
||||
|
||||
const commentCount = this.extractCommentCount(element, selectors.comment);
|
||||
if (commentCount) {
|
||||
facts.push(NewFact('li_post_comment_count', 'number', commentCount, contextUrl));
|
||||
}
|
||||
|
||||
const shareCount = this.extractShareCount(element, selectors.share);
|
||||
if (shareCount) {
|
||||
facts.push(NewFact('li_post_share_count', 'number', shareCount, contextUrl));
|
||||
}
|
||||
|
||||
const date = this.extractDate(selectors.date)
|
||||
// example value: "3hr"
|
||||
if (date) {
|
||||
facts.push(NewFact('li_post_duration_string', 'string', date, contextUrl));
|
||||
}
|
||||
|
||||
return facts;
|
||||
}
|
||||
|
||||
private extractCommentFromElement(element: HTMLElement, selectors: typeof ELEMENT_SELECTORS.comment): model.Fact[] {
|
||||
const profileUrlEl = element.querySelector<HTMLElement>(selectors.urlProfile);
|
||||
const profileUrl = profileUrlEl?.getAttribute('href')
|
||||
const cleanedUrl = profileUrl?.replace('https://www.linkedin.com/in/', '').replace("/", "")
|
||||
|
||||
const contextUrl = `${window.location.href.replace(/\/$/, '')}#comment_by=${cleanedUrl}`;
|
||||
const facts: model.Fact[] = [];
|
||||
|
||||
const displayNameEl = element.querySelector<HTMLElement>(selectors.displayName);
|
||||
if (displayNameEl?.textContent?.trim()) {
|
||||
facts.push(NewFact('li_comment_display_name', 'name', displayNameEl.textContent.trim(), contextUrl));
|
||||
}
|
||||
|
||||
const profileImageEl = element.querySelector<HTMLElement>(selectors.profileImage);
|
||||
if (profileImageEl?.getAttribute('src')) {
|
||||
const profileImageUrl = profileImageEl.getAttribute('src') || '';
|
||||
facts.push(NewFact('li_comment_profileimage_url', 'url', profileImageUrl, contextUrl));
|
||||
}
|
||||
|
||||
const content = this.extractContent(element, [...selectors.content]);
|
||||
if (content) {
|
||||
facts.push(NewFact('li_comment_content_md', 'md', content, contextUrl));
|
||||
}
|
||||
|
||||
const likeEl = element.querySelector<HTMLElement>(selectors.like);
|
||||
if (likeEl?.textContent?.trim()) {
|
||||
// Hapus semua karakter non-digit kecuali titik dan koma, lalu hapus pemisah ribuan
|
||||
const cleanedText = likeEl.textContent.trim().replace(/[^\d.,]/g, '');
|
||||
const numericValue = cleanedText.replace(/[.,]/g, ''); // Hapus titik dan koma
|
||||
const likeCount = Number(numericValue) || 0;
|
||||
facts.push(NewFact('li_comment_like_count', 'number', likeCount.toString(), contextUrl));
|
||||
} else {
|
||||
facts.push(NewFact('li_comment_like_count', 'number', "0", contextUrl));
|
||||
}
|
||||
|
||||
const commentCountEl = element.querySelector<HTMLElement>(selectors.commentCount);
|
||||
if (commentCountEl?.textContent?.trim()) {
|
||||
const commentText = commentCountEl.textContent.trim().split(' ')[0];
|
||||
// Hapus semua karakter non-digit kecuali titik dan koma, lalu hapus pemisah ribuan
|
||||
const cleanedText = commentText.replace(/[^\d.,]/g, '');
|
||||
const numericValue = cleanedText.replace(/[.,]/g, ''); // Hapus titik dan koma
|
||||
const commentCount = Number(numericValue) || 0;
|
||||
facts.push(NewFact('li_comment_comment_count', 'number', commentCount.toString(), contextUrl));
|
||||
} else {
|
||||
facts.push(NewFact('li_comment_comment_count', 'number', "0", contextUrl));
|
||||
}
|
||||
|
||||
const dateEl = element.querySelector<HTMLElement>(selectors.date);
|
||||
if (dateEl && dateEl.textContent) {
|
||||
const dateText = dateEl.textContent.trim();
|
||||
facts.push(NewFact('li_comment_duration_string', 'string', dateText, contextUrl));
|
||||
}
|
||||
|
||||
return facts;
|
||||
}
|
||||
|
||||
private extractDate(selector: string): string {
|
||||
const dateEl = document.querySelector<HTMLElement>(selector);
|
||||
if (dateEl && dateEl.textContent) {
|
||||
const dateText = dateEl.textContent.trim();
|
||||
return dateText;
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
private extractContent(element: HTMLElement, contentSelectors: string[]): string {
|
||||
let textContent = '';
|
||||
const mediaUrls: string[] = [];
|
||||
|
||||
for (const contentSelector of contentSelectors) {
|
||||
const contentEls = element.querySelectorAll(contentSelector);
|
||||
|
||||
contentEls.forEach(el => {
|
||||
if (!el.querySelector('img') && !el.querySelector('video') &&
|
||||
el.tagName !== 'IMG' && el.tagName !== 'VIDEO') {
|
||||
if (el.textContent && el.textContent.trim() !== '') {
|
||||
textContent += el.outerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
this.extractVideoUrls(el, mediaUrls);
|
||||
this.extractImageUrls(el, mediaUrls);
|
||||
});
|
||||
}
|
||||
|
||||
const markdownText = textContent ? turndownService.turndown(textContent) : '';
|
||||
const uniqueMediaUrls = this.cleanMediaUrls(mediaUrls);
|
||||
|
||||
let mediaContent = '';
|
||||
if (uniqueMediaUrls.length > 0) {
|
||||
mediaContent = uniqueMediaUrls.map((url: string) => `[photo](${url})`).join('\n');
|
||||
}
|
||||
|
||||
return mediaContent + markdownText;
|
||||
}
|
||||
|
||||
private extractLikeCount(element: HTMLElement, likeSelectors: string[]): string | null {
|
||||
for (const likeSelector of likeSelectors) {
|
||||
const likeEl = element.querySelector<HTMLElement>(likeSelector);
|
||||
if (likeEl?.textContent?.trim()) {
|
||||
// Hapus semua karakter non-digit kecuali titik dan koma, lalu hapus pemisah ribuan
|
||||
const cleanedText = likeEl.textContent.trim().replace(/[^\d.,]/g, '');
|
||||
const numericValue = cleanedText.replace(/[.,]/g, ''); // Hapus titik dan koma
|
||||
const likeCount = Number(numericValue) || 0;
|
||||
return likeCount.toString();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private extractCommentCount(element: HTMLElement, commentSelector: string): string | null {
|
||||
const commentEl = element.querySelector<HTMLElement>(commentSelector);
|
||||
if (commentEl?.getAttribute('aria-label')) {
|
||||
const ariaLabel = commentEl.getAttribute('aria-label')?.split(' ')[0].trim() || '0';
|
||||
// Hapus semua karakter non-digit kecuali titik dan koma, lalu hapus pemisah ribuan
|
||||
const cleanedText = ariaLabel.replace(/[^\d.,]/g, '');
|
||||
const numericValue = cleanedText.replace(/[.,]/g, ''); // Hapus titik dan koma
|
||||
const commentCount = Number(numericValue) || 0;
|
||||
return commentCount.toString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private extractShareCount(element: HTMLElement, shareSelector: string): string | null {
|
||||
const shareEl = element.querySelector<HTMLElement>(shareSelector);
|
||||
if (shareEl?.textContent?.trim()) {
|
||||
const shareText = shareEl.textContent.trim().split(' ')[0] || '0';
|
||||
// Hapus semua karakter non-digit kecuali titik dan koma, lalu hapus pemisah ribuan
|
||||
const cleanedText = shareText.replace(/[^\d.,]/g, '');
|
||||
const numericValue = cleanedText.replace(/[.,]/g, ''); // Hapus titik dan koma
|
||||
const shareCount = Number(numericValue) || 0;
|
||||
return shareCount.toString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private extractVideoUrls(element: Element, mediaUrls: string[]): void {
|
||||
if (element.tagName === 'VIDEO') {
|
||||
const posterUrl = element.getAttribute('poster');
|
||||
if (posterUrl && !mediaUrls.includes(posterUrl)) {
|
||||
mediaUrls.push(posterUrl);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const videoElements = element.querySelectorAll('video');
|
||||
videoElements.forEach(videoEl => {
|
||||
const posterUrl = videoEl.getAttribute('poster');
|
||||
if (posterUrl && !mediaUrls.includes(posterUrl)) {
|
||||
mediaUrls.push(posterUrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private extractImageUrls(element: Element, mediaUrls: string[]): void {
|
||||
if (element.tagName === 'IMG') {
|
||||
const imgSrc = element.getAttribute('src');
|
||||
if (imgSrc && !mediaUrls.includes(imgSrc)) {
|
||||
mediaUrls.push(imgSrc);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const imgElements = element.querySelectorAll('img');
|
||||
imgElements.forEach(imgEl => {
|
||||
const imgSrc = imgEl.getAttribute('src');
|
||||
if (imgSrc && !mediaUrls.includes(imgSrc)) {
|
||||
mediaUrls.push(imgSrc);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private cleanMediaUrl(url: string): string {
|
||||
try {
|
||||
return url.split('?')[0];
|
||||
} catch (e) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
private cleanMediaUrls(urls: string[]): string[] {
|
||||
const uniqueUrls: string[] = [];
|
||||
|
||||
for (const url of urls) {
|
||||
if (!uniqueUrls.includes(url)) {
|
||||
uniqueUrls.push(url);
|
||||
}
|
||||
}
|
||||
|
||||
const cleanedUrls: string[] = [];
|
||||
const cleanedBaseUrls: string[] = [];
|
||||
|
||||
for (const url of uniqueUrls) {
|
||||
const baseUrl = this.cleanMediaUrl(url);
|
||||
|
||||
if (!cleanedBaseUrls.includes(baseUrl)) {
|
||||
cleanedBaseUrls.push(baseUrl);
|
||||
cleanedUrls.push(url);
|
||||
}
|
||||
}
|
||||
|
||||
return cleanedUrls;
|
||||
}
|
||||
|
||||
isTagged(element: HTMLElement): boolean {
|
||||
return element.classList.contains('tagged');
|
||||
}
|
||||
|
||||
isSaved(element: HTMLElement): boolean {
|
||||
return element.classList.contains('saved');
|
||||
}
|
||||
}
|
||||
705
src/modules/tiktok.ts
Normal file
705
src/modules/tiktok.ts
Normal file
@@ -0,0 +1,705 @@
|
||||
import * as model from "../model";
|
||||
import { NewFact } from "../model";
|
||||
import { turndownService } from "../utils/turndown";
|
||||
import { parseCount } from "../helper/parseCount";
|
||||
|
||||
const ELEMENT_SELECTORS = {
|
||||
profile: {
|
||||
ld: 'script[id="__UNIVERSAL_DATA_FOR_REHYDRATION__"]',
|
||||
},
|
||||
post: {
|
||||
container: "div[data-e2e$='item-list'] div[data-e2e$='item']",
|
||||
url: "div[data-e2e$='post-item'] a[href*='/video/'], div[data-e2e$='post-item'] a[href*='/photo/'], div[data-e2e$='search_top-item'] a[href*='/@']",
|
||||
profileImage: '[data-e2e="browse-user-avatar"] img[src]',
|
||||
view: '[data-e2e="video-views"]',
|
||||
content: ["[data-e2e='browse-video'] > div", "[data-e2e='browse-video-desc']", "picture > img"],
|
||||
username: '[data-e2e="browse-username"]',
|
||||
displayName: '[data-e2e="browser-nickname"] span:first-child',
|
||||
date: '[data-e2e="browser-nickname"] span:last-child',
|
||||
like: '[data-e2e="browse-like-count"], [data-e2e="like-count"]',
|
||||
share: '[data-e2e="share-count"]',
|
||||
comment: '[data-e2e="browse-comment-count"], [data-e2e="comment-count"]',
|
||||
},
|
||||
postVideo: {
|
||||
container: "div[class*='DivBrowserModeContainer']",
|
||||
profileImage: '[data-e2e="browse-user-avatar"] img[src]',
|
||||
content: "[data-e2e='browse-video-desc']",
|
||||
videoUrl: "[data-e2e='browse-video'] > div > video[src",
|
||||
view: `a[href*="${window.location.href}"] [data-e2e="video-views"]`,
|
||||
username: 'div[class*="DivDescriptionContentWrapper"] a:nth-child(2) > span:first-child',
|
||||
displayName: 'div[class*="DivDescriptionContentWrapper"] a:nth-child(2) > span:nth-child(3) > span:first-child',
|
||||
date: 'div[class*="DivDescriptionContentWrapper"] a:nth-child(2) > span:nth-child(3) > span:last-child',
|
||||
likeCount: '[data-e2e="browse-like-count"], [data-e2e="like-count"]',
|
||||
commentCount: '[data-e2e="browse-comment-count"], [data-e2e="comment-count"]',
|
||||
shareCount: '[data-e2e="share-count"]',
|
||||
bookmarkCount: '[data-e2e="undefined-count"]',
|
||||
},
|
||||
postPhoto: {
|
||||
container: "[class*='DivBrowserModeContainer'], [class*='DivPlayerContainer']",
|
||||
content: "[data-e2e='browse-video-desc']",
|
||||
photoUrl: "[class*='DivPhotoVideoContainer']",
|
||||
profileImage: '[data-e2e="browse-user-avatar"] img[src]',
|
||||
username: 'div[class*="DivDescriptionContentWrapper"] a:nth-child(2) > span:first-child, [data-e2e="browse-username"]',
|
||||
displayName: 'div[class*="DivDescriptionContentWrapper"] a:nth-child(2) > span:nth-child(3) > span:first-child, [data-e2e="browser-nickname"] span:first-child',
|
||||
date: 'div[class*="DivDescriptionContentWrapper"] a:nth-child(2) > span:nth-child(3) > span:last-child, [data-e2e="browser-nickname"] span:last-child',
|
||||
like: '[data-e2e="browse-like-count"], [data-e2e="like-count"]',
|
||||
view: `a[href*="${window.location.href}"] [data-e2e="video-views"]`,
|
||||
share: '[data-e2e="share-count"]',
|
||||
bookmarkCount: '[data-e2e="undefined-count"]',
|
||||
comment: '[data-e2e="browse-comment-count"], [data-e2e="comment-count"]',
|
||||
},
|
||||
comment: {
|
||||
container: 'div[class*="DivCommentItemContainer"], div[class*="DivCommentObjectWrapper"]',
|
||||
url: 'div[class*="DivContentContainer"] > a',
|
||||
content: '[data-e2e="comment-level-1"] span, [data-e2e="comment-level-1"] p',
|
||||
username: '[data-e2e="comment-avatar-1"], [data-e2e="comment-username-1"] > div > a ',
|
||||
displayName: '[data-e2e="comment-username-1"]',
|
||||
date: '[data-e2e="comment-time-1"], [class*="DivCommentSubContentWrapper"] > span:first-child',
|
||||
commentCount: '[class*="DivReplyActionContainer"] > p, [class*="DivViewRepliesContainer"] > span',
|
||||
like: '[data-e2e="comment-like-count"], [aria-label*="likes"] span',
|
||||
}
|
||||
} as const;
|
||||
|
||||
export default class TiktokModule {
|
||||
context: string = "";
|
||||
|
||||
tagElement(): void {
|
||||
Object.keys(ELEMENT_SELECTORS).forEach((type) => {
|
||||
const selectorObj = ELEMENT_SELECTORS[type as keyof typeof ELEMENT_SELECTORS];
|
||||
Object.entries(selectorObj).forEach(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(sel => {
|
||||
document.querySelectorAll<HTMLElement>(sel).forEach(element => this.addClassTagged(element));
|
||||
});
|
||||
} else {
|
||||
document.querySelectorAll<HTMLElement>(value).forEach(element => this.addClassTagged(element));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async saveData(): Promise<model.FactCollection[]> {
|
||||
const factCollections: model.FactCollection[] = [];
|
||||
|
||||
try {
|
||||
if (this.isSearchPage()) {
|
||||
console.log("Detected search page");
|
||||
await this.handleSearchPage(factCollections);
|
||||
} else if (this.isProfilePage()) {
|
||||
|
||||
await this.handleProfilePage(factCollections);
|
||||
} else if (this.isVideoPage()) {
|
||||
await this.handleVideoPage(factCollections);
|
||||
} else if (this.isPhotoPage()) {
|
||||
await this.handlePhotoPage(factCollections);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in saveData:", error);
|
||||
}
|
||||
|
||||
return factCollections;
|
||||
}
|
||||
|
||||
private async handleSearchPage(factCollections: model.FactCollection[]): Promise<void> {
|
||||
const posts = await this.savePost();
|
||||
if (posts.facts.length > 0) {
|
||||
factCollections.push(posts);
|
||||
this.markElementSaved(ELEMENT_SELECTORS.post.container);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
private isProfilePage(): boolean {
|
||||
return !window.location.href.includes('/video/') && !window.location.href.includes('/photo/');
|
||||
}
|
||||
|
||||
private isSearchPage(): boolean {
|
||||
return window.location.href.includes('/search');
|
||||
}
|
||||
|
||||
private isVideoPage(): boolean {
|
||||
return window.location.href.includes('/video/');
|
||||
}
|
||||
|
||||
private isPhotoPage(): boolean {
|
||||
return window.location.href.includes('/photo/');
|
||||
}
|
||||
|
||||
private async handleProfilePage(factCollections: model.FactCollection[]): Promise<void> {
|
||||
const profile = this.saveProfile();
|
||||
if (profile.facts.length > 0) {
|
||||
factCollections.push(profile);
|
||||
}
|
||||
|
||||
const posts = await this.savePost();
|
||||
if (posts.facts.length > 0) {
|
||||
factCollections.push(posts);
|
||||
this.markElementSaved(ELEMENT_SELECTORS.post.container);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleVideoPage(factCollections: model.FactCollection[]): Promise<void> {
|
||||
const posts = await this.savePost();
|
||||
if (posts.facts.length > 0) {
|
||||
factCollections.push(posts);
|
||||
this.markElementSaved(ELEMENT_SELECTORS.post.container);
|
||||
}
|
||||
|
||||
const comments = this.saveComment();
|
||||
if (comments.facts.length > 0) {
|
||||
factCollections.push(comments);
|
||||
this.markElementSaved(ELEMENT_SELECTORS.comment.container);
|
||||
}
|
||||
}
|
||||
|
||||
private async handlePhotoPage(factCollections: model.FactCollection[]): Promise<void> {
|
||||
const posts = await this.savePost();
|
||||
if (posts.facts.length > 0) {
|
||||
factCollections.push(posts);
|
||||
this.markElementSaved(ELEMENT_SELECTORS.post.container);
|
||||
}
|
||||
|
||||
const comments = this.saveComment();
|
||||
if (comments.facts.length > 0) {
|
||||
factCollections.push(comments);
|
||||
this.markElementSaved(ELEMENT_SELECTORS.comment.container);
|
||||
}
|
||||
}
|
||||
|
||||
private markElementSaved(selector: string): void {
|
||||
const element = document.querySelector<HTMLElement>(selector) || document.body;
|
||||
this.addClassSaved(element);
|
||||
}
|
||||
|
||||
saveProfile(): model.FactCollection {
|
||||
const profile: model.FactCollection = model.NewFactCollection('profile');
|
||||
|
||||
profile.facts.push(NewFact('tt_profile_source_url', 'url', window.location.href, window.location.href));
|
||||
const scriptTag = document.querySelector<HTMLScriptElement>(`${ELEMENT_SELECTORS.profile.ld}:not(.saved)`);
|
||||
if (!scriptTag || !scriptTag.textContent) {
|
||||
return profile;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(scriptTag.textContent);
|
||||
const userInfo = data?.__DEFAULT_SCOPE__?.['webapp.user-detail']?.userInfo;
|
||||
if (userInfo && userInfo.user) {
|
||||
// Direct NewFact calls for profile data
|
||||
const username = userInfo.user?.uniqueId || '';
|
||||
if (username) {
|
||||
profile.facts.push(NewFact('tt_profile_user_name', 'name', username, window.location.href));
|
||||
}
|
||||
|
||||
const displayName = userInfo.user?.nickname || '';
|
||||
if (displayName) {
|
||||
profile.facts.push(NewFact('tt_profile_display_name', 'name', displayName, window.location.href));
|
||||
}
|
||||
|
||||
const description = userInfo.user?.signature || '';
|
||||
if (description) {
|
||||
profile.facts.push(NewFact('tt_profile_bio_text', 'text', description, window.location.href));
|
||||
}
|
||||
|
||||
const image = userInfo.user?.avatarLarger || userInfo.user?.avatarThumb || '';
|
||||
if (image) {
|
||||
profile.facts.push(NewFact('tt_profile_profileimage_url', 'url', image, window.location.href));
|
||||
}
|
||||
|
||||
const followerCount = userInfo.stats?.followerCount?.toString() || '0';
|
||||
profile.facts.push(NewFact('tt_profile_follower_count', 'number', followerCount, window.location.href));
|
||||
|
||||
const followingCount = userInfo.stats?.followingCount?.toString() || '0';
|
||||
profile.facts.push(NewFact('tt_profile_following_count', 'number', followingCount, window.location.href));
|
||||
|
||||
const postCount = userInfo.stats?.videoCount?.toString() || '0';
|
||||
profile.facts.push(NewFact('tt_profile_post_count', 'number', postCount, window.location.href));
|
||||
|
||||
const likeCount = userInfo.stats?.heartCount?.toString() || '0';
|
||||
profile.facts.push(NewFact('tt_profile_like_count', 'number', likeCount, window.location.href));
|
||||
|
||||
this.addClassSaved(scriptTag);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing TikTok profile:", error);
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
async savePost(): Promise<model.FactCollection> {
|
||||
const posts: model.FactCollection = window.location.href.includes('/search/') ? model.NewFactCollection('search') : model.NewFactCollection('post');
|
||||
|
||||
if (this.isVideoPage()) {
|
||||
await this.extractVideoPost(posts);
|
||||
} else if (this.isPhotoPage()) {
|
||||
await this.extractPhotoPost(posts);
|
||||
} else {
|
||||
await this.extractAllPosts(posts);
|
||||
}
|
||||
|
||||
return posts;
|
||||
}
|
||||
|
||||
private async extractVideoPost(posts: model.FactCollection): Promise<void> {
|
||||
const scriptTag = document.querySelector<HTMLScriptElement>("script#__UNIVERSAL_DATA_FOR_REHYDRATION__:not(.saved)");
|
||||
let dataExtracted = false;
|
||||
|
||||
if (scriptTag && scriptTag.textContent) {
|
||||
try {
|
||||
const data = JSON.parse(scriptTag.textContent);
|
||||
const stats = data?.['__DEFAULT_SCOPE__']['webapp.video-detail']["itemInfo"]["itemStruct"] ||
|
||||
data?.['__DEFAULT_SCOPE__']['webapp.video-detail']["itemInfo"]["itemStruct"]["stats"];
|
||||
|
||||
const url = window.location.href;
|
||||
const urlInJson = data?.['__DEFAULT_SCOPE__']['seo.abtest']['canonical'];
|
||||
|
||||
if (stats && url === urlInJson) {
|
||||
this.createPostFacts(stats, posts, url);
|
||||
this.addClassSaved(document.body);
|
||||
dataExtracted = true;
|
||||
scriptTag.classList.add('saved');
|
||||
} else {
|
||||
console.warn("No stats found in video detail data or URL mismatch");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing TikTok video post:", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (!dataExtracted) {
|
||||
this.extractVideoPostFromDOM(posts);
|
||||
}
|
||||
}
|
||||
|
||||
private extractVideoPostFromDOM(posts: model.FactCollection): void {
|
||||
const url = window.location.href;
|
||||
const container = document.querySelector<HTMLElement>(`${ELEMENT_SELECTORS.postVideo.container}:not(.saved)`);
|
||||
const selectors = ELEMENT_SELECTORS.postVideo;
|
||||
|
||||
if (!container) {
|
||||
console.warn("❌ No container found for video post extraction or already saved");
|
||||
return;
|
||||
}
|
||||
|
||||
posts.facts.push(NewFact('tt_post_source_url', 'url', url, url));
|
||||
|
||||
const displayName = this.getDisplayName(container, selectors.displayName) ||
|
||||
this.getDisplayName(document, selectors.displayName);
|
||||
if (displayName) {
|
||||
posts.facts.push(NewFact('tt_post_display_name', 'name', displayName, url));
|
||||
}
|
||||
|
||||
const username = this.getUsername(container, selectors.username) ||
|
||||
this.getUsername(document, selectors.username);
|
||||
if (username) {
|
||||
posts.facts.push(NewFact('tt_post_user_name', 'name', username, url));
|
||||
|
||||
posts.facts.push(NewFact('tt_post_profile_url', 'url', `https://www.tiktok.com/@${this.extractUsernameFromUrl(username)}`, url));
|
||||
}
|
||||
|
||||
|
||||
|
||||
const profileImageElement = document.querySelector<HTMLImageElement>(selectors.profileImage);
|
||||
if (profileImageElement?.src) {
|
||||
posts.facts.push(NewFact('tt_post_profileimage_url', 'url', profileImageElement.src, url));
|
||||
}
|
||||
|
||||
const content = this.getContent(container, selectors.content) ||
|
||||
this.getContent(document, selectors.content);
|
||||
if (content) {
|
||||
posts.facts.push(NewFact('tt_post_content_md', 'md', content, url));
|
||||
}
|
||||
|
||||
const viewElement = document.querySelector<HTMLElement>(`a[href="${window.location.href}"] [data-e2e="video-views"]`) ||
|
||||
document.querySelector<HTMLElement>(selectors.view);
|
||||
if (viewElement?.textContent) {
|
||||
const viewCount = parseCount(viewElement.textContent.trim());
|
||||
posts.facts.push(NewFact('tt_post_view_count', 'number', viewCount, url));
|
||||
}
|
||||
|
||||
const likeCount = this.getInteractionCount(container, selectors.likeCount) ||
|
||||
this.getInteractionCount(document, selectors.likeCount);
|
||||
if (likeCount) {
|
||||
posts.facts.push(NewFact('tt_post_like_count', 'number', likeCount, url));
|
||||
}
|
||||
|
||||
const shareCount = this.getInteractionCount(container, selectors.shareCount) ||
|
||||
this.getInteractionCount(document, selectors.shareCount);
|
||||
if (shareCount) {
|
||||
posts.facts.push(NewFact('tt_post_share_count', 'number', shareCount, url));
|
||||
}
|
||||
|
||||
const commentCount = this.getInteractionCount(container, selectors.commentCount) ||
|
||||
this.getInteractionCount(document, selectors.commentCount);
|
||||
if (commentCount) {
|
||||
posts.facts.push(NewFact('tt_post_comment_count', 'number', commentCount, url));
|
||||
}
|
||||
|
||||
const bookmarkCount = this.getInteractionCount(container, selectors.bookmarkCount) ||
|
||||
this.getInteractionCount(document, selectors.bookmarkCount);
|
||||
if (bookmarkCount) {
|
||||
posts.facts.push(NewFact('tt_post_bookmark_count', 'number', bookmarkCount, url));
|
||||
}
|
||||
|
||||
const date = container.querySelector<HTMLElement>(selectors.date)?.textContent?.trim();
|
||||
if (date) {
|
||||
posts.facts.push(NewFact('tt_post_duration_string', 'string', date, url));
|
||||
}
|
||||
|
||||
this.addClassSaved(container);
|
||||
}
|
||||
|
||||
private extractUsernameFromUrl(url: string): string {
|
||||
const match = url.match(/@([^\/]+)/);
|
||||
return match ? match[1] : 'unknown';
|
||||
}
|
||||
|
||||
private async extractPhotoPost(posts: model.FactCollection): Promise<void> {
|
||||
const container = document.querySelector<HTMLElement>(`${ELEMENT_SELECTORS.postPhoto.container}:not(.saved)`);
|
||||
|
||||
if (!container) {
|
||||
console.warn("❌ No container found for photo post extraction or already saved");
|
||||
return;
|
||||
}
|
||||
|
||||
const url = window.location.href;
|
||||
const selectors = ELEMENT_SELECTORS.postPhoto;
|
||||
|
||||
posts.facts.push(NewFact('tt_post_source_url', 'url', url, url));
|
||||
|
||||
const displayName = this.getDisplayName(document, selectors.displayName);
|
||||
if (displayName) posts.facts.push(NewFact('tt_post_display_name', 'name', displayName, url));
|
||||
|
||||
const username = this.getUsername(document, selectors.username);
|
||||
if (username) posts.facts.push(NewFact('tt_post_user_name', 'name', username, url));
|
||||
|
||||
const profileImageElement = document.querySelector<HTMLImageElement>(selectors.profileImage);
|
||||
if (profileImageElement?.src) {
|
||||
posts.facts.push(NewFact('tt_post_profileimage_url', 'url', profileImageElement.src, url));
|
||||
}
|
||||
|
||||
const content = (container ? this.getContent(container, selectors.content) : null) ||
|
||||
this.getContent(document, selectors.content);
|
||||
if (content) posts.facts.push(NewFact('tt_post_content_md', 'md', content, url));
|
||||
|
||||
const viewCount = document.querySelector<HTMLElement>(selectors.view)?.textContent?.trim();
|
||||
const viewCountText = viewCount ? parseCount(viewCount) : "0";
|
||||
if (viewCount) posts.facts.push(NewFact('tt_post_view_count', 'number', viewCountText, url));
|
||||
|
||||
const likeCount = this.getInteractionCount(document, selectors.like);
|
||||
if (likeCount) posts.facts.push(NewFact('tt_post_like_count', 'number', likeCount, url));
|
||||
|
||||
const shareCount = this.getInteractionCount(document, selectors.share);
|
||||
if (shareCount) posts.facts.push(NewFact('tt_post_share_count', 'number', shareCount, url));
|
||||
|
||||
const commentCount = this.getInteractionCount(document, selectors.comment);
|
||||
if (commentCount) posts.facts.push(NewFact('tt_post_comment_count', 'number', commentCount, url));
|
||||
|
||||
const bookmarkCount = this.getInteractionCount(document, selectors.bookmarkCount);
|
||||
if (bookmarkCount) posts.facts.push(NewFact('tt_post_bookmark_count', 'number', bookmarkCount, url));
|
||||
|
||||
const date = container.querySelector<HTMLElement>(selectors.date)?.textContent?.trim() || "";
|
||||
if (date) posts.facts.push(NewFact('tt_post_duration_string', 'string', date, url));
|
||||
|
||||
this.addClassSaved(container);
|
||||
}
|
||||
|
||||
private extractAllPosts(posts: model.FactCollection): void {
|
||||
const postElements = document.querySelectorAll<HTMLElement>('[id^="column-item-video-container"]:not(.saved)');
|
||||
|
||||
for (const element of postElements) {
|
||||
let postUrl = '';
|
||||
const fullUrlEl = element.querySelector<HTMLElement>(ELEMENT_SELECTORS.post.url);
|
||||
if (fullUrlEl) {
|
||||
const href = fullUrlEl.getAttribute('href') || '';
|
||||
postUrl = href.startsWith('http') ? href : `https://www.tiktok.com${href}`;
|
||||
posts.facts.push(NewFact('tt_post_source_url', 'url', href, href));
|
||||
}
|
||||
|
||||
|
||||
const profileUrl = element.querySelector<HTMLElement>('[data-e2e="search-card-user-link"]')?.getAttribute('href') || '';
|
||||
if (profileUrl) {
|
||||
const profileLink = profileUrl.startsWith('http') ? profileUrl : `https://www.tiktok.com${profileUrl}`;
|
||||
posts.facts.push(NewFact('tt_post_profile_url', 'url', profileLink, postUrl));
|
||||
}else if(!window.location.href.includes('/search/')){
|
||||
const profileLink = window.location.href;
|
||||
posts.facts.push(NewFact('tt_post_profile_url', 'url', profileLink, postUrl));
|
||||
}
|
||||
|
||||
const userName = element.querySelector<HTMLElement>('[data-e2e="search-card-user-link"]')?.getAttribute('href')?.replace('@', '').replace("/", '').trim();
|
||||
if (userName) {
|
||||
posts.facts.push(NewFact('tt_post_user_name', 'name', userName, postUrl));
|
||||
}else if(!window.location.href.includes('/search/')){
|
||||
const userName = window.location.href.split('@').pop() || '';
|
||||
posts.facts.push(NewFact('tt_post_user_name', 'name', userName, postUrl));
|
||||
}
|
||||
this.addClassSaved(element);
|
||||
}
|
||||
}
|
||||
|
||||
saveComment(): model.FactCollection {
|
||||
const comments: model.FactCollection = model.NewFactCollection('comment');
|
||||
const selectors = ELEMENT_SELECTORS.comment;
|
||||
const commentElements = document.querySelectorAll<HTMLElement>(`${selectors.container}:not(.saved)`);
|
||||
|
||||
commentElements.forEach((element) => {
|
||||
if (element.classList.contains('saved')) {
|
||||
console.warn("❌ Comment element already saved, skipping:", element);
|
||||
return;
|
||||
}
|
||||
|
||||
const elements = {
|
||||
displayName: element.querySelector<HTMLElement>(selectors.displayName),
|
||||
username: element.querySelector<HTMLElement>(selectors.username),
|
||||
content: element.querySelector<HTMLElement>(selectors.content),
|
||||
likeCount: element.querySelector<HTMLElement>(selectors.like),
|
||||
commentCount: element.querySelector<HTMLElement>(selectors.commentCount),
|
||||
date: element.querySelector<HTMLElement>(selectors.date),
|
||||
};
|
||||
|
||||
if (elements.displayName && elements.content) {
|
||||
this.extractCommentFacts(elements, comments, element);
|
||||
element.classList.add('saved');
|
||||
}
|
||||
});
|
||||
|
||||
return comments;
|
||||
}
|
||||
|
||||
private extractCommentFacts(
|
||||
elements: {
|
||||
displayName: HTMLElement | null;
|
||||
username: HTMLElement | null;
|
||||
content: HTMLElement | null;
|
||||
likeCount: HTMLElement | null;
|
||||
commentCount: HTMLElement | null;
|
||||
date: HTMLElement | null;
|
||||
},
|
||||
comments: model.FactCollection,
|
||||
element: HTMLElement
|
||||
): void {
|
||||
const username = elements.username?.getAttribute('href')?.replace('@', '').replace("/", '').trim();
|
||||
const contextUrl = window.location.href + `#comment_by=${username}`;
|
||||
|
||||
const displayName = elements.displayName?.innerText.trim();
|
||||
if (displayName) {
|
||||
comments.facts.push(NewFact('tt_comment_display_name', 'name', displayName, contextUrl));
|
||||
}
|
||||
|
||||
if (username) {
|
||||
comments.facts.push(NewFact('tt_comment_user_name', 'name', username.replace('@', '').replace("/", ''), contextUrl));
|
||||
}
|
||||
|
||||
const content = elements.content?.innerText.trim();
|
||||
if (content) {
|
||||
comments.facts.push(NewFact('tt_comment_content_md', 'md', content, contextUrl));
|
||||
}
|
||||
|
||||
const likeCount = elements.likeCount ? parseCount(elements.likeCount.innerText.trim()) : "0";
|
||||
comments.facts.push(NewFact('tt_comment_like_count', 'number', likeCount, contextUrl));
|
||||
|
||||
if (elements.commentCount) {
|
||||
const commentCountText = elements.commentCount.innerText.trim();
|
||||
const digitsOnly = commentCountText.split(" ")[1];
|
||||
const commentCount = parseCount(digitsOnly);
|
||||
if (commentCount) {
|
||||
comments.facts.push(NewFact('tt_comment_comment_count', 'number', commentCount, contextUrl));
|
||||
}
|
||||
}
|
||||
|
||||
const dateText = elements.date?.textContent?.trim();
|
||||
if (dateText) {
|
||||
comments.facts.push(NewFact('tt_comment_duration_string', 'string', dateText, contextUrl));
|
||||
}
|
||||
}
|
||||
|
||||
private createPostFacts(itemStruct: any, posts: model.FactCollection, url: string): void {
|
||||
posts.facts.push(NewFact('tt_post_source_url', 'url', url, url));
|
||||
|
||||
let displayName = '';
|
||||
let username = '';
|
||||
|
||||
const author = itemStruct.statsV2?.author || itemStruct.author || {};
|
||||
|
||||
if (author.nickname) {
|
||||
displayName = author.nickname;
|
||||
} else {
|
||||
displayName = this.extractUsernameFromUrl(url);
|
||||
}
|
||||
|
||||
if (author.uniqueId) {
|
||||
username = author.uniqueId;
|
||||
} else {
|
||||
username = this.extractUsernameFromUrl(url);
|
||||
}
|
||||
|
||||
if (displayName) {
|
||||
posts.facts.push(NewFact('tt_post_display_name', 'name', displayName, url));
|
||||
}
|
||||
if (username) {
|
||||
posts.facts.push(NewFact('tt_post_user_name', 'name', username, url));
|
||||
}
|
||||
|
||||
if (itemStruct.author.avatarLarger) {
|
||||
posts.facts.push(NewFact('tt_post_profileimage_url', 'url', itemStruct.author.avatarLarger, url));
|
||||
}
|
||||
|
||||
if (itemStruct.desc) {
|
||||
let contentFormat = '';
|
||||
const videoUrl = itemStruct.video?.bitrateInfo?.[0]?.PlayAddr?.UrlList?.[1] ||
|
||||
itemStruct.video?.playAddr || '';
|
||||
const content = itemStruct.desc.trim();
|
||||
|
||||
if (videoUrl && content) {
|
||||
contentFormat = `[videoUrl](${videoUrl})\n\n${content}`;
|
||||
} else if (videoUrl) {
|
||||
contentFormat = `[videoUrl](${videoUrl})`;
|
||||
} else if (content) {
|
||||
contentFormat = turndownService.turndown(content);
|
||||
}
|
||||
|
||||
if (contentFormat) {
|
||||
posts.facts.push(NewFact('tt_post_content_md', 'md', contentFormat, url));
|
||||
}
|
||||
}
|
||||
|
||||
const stats = itemStruct.statsV2 || itemStruct.stats || {};
|
||||
|
||||
if (stats.diggCount !== undefined) {
|
||||
const likeCount = parseCount(stats.diggCount.toString());
|
||||
posts.facts.push(NewFact('tt_post_like_count', 'number', likeCount, url));
|
||||
}
|
||||
|
||||
if (stats.commentCount !== undefined) {
|
||||
const commentCount = parseCount(stats.commentCount.toString());
|
||||
posts.facts.push(NewFact('tt_post_comment_count', 'number', commentCount, url));
|
||||
}
|
||||
|
||||
if (stats.shareCount !== undefined) {
|
||||
const shareCount = parseCount(stats.shareCount.toString());
|
||||
posts.facts.push(NewFact('tt_post_share_count', 'number', shareCount, url));
|
||||
}
|
||||
|
||||
if (stats.playCount !== undefined) {
|
||||
const viewCount = parseCount(stats.playCount.toString());
|
||||
posts.facts.push(NewFact('tt_post_view_count', 'number', viewCount, url));
|
||||
}
|
||||
|
||||
if (stats.collectCount !== undefined) {
|
||||
const bookmarkCount = parseCount(stats.collectCount.toString());
|
||||
posts.facts.push(NewFact('tt_post_bookmark_count', 'number', bookmarkCount, url));
|
||||
}
|
||||
|
||||
if (itemStruct.createTime) {
|
||||
const date = new Date(itemStruct.createTime * 1000).toISOString();
|
||||
posts.facts.push(NewFact('tt_post_createdat_datetime', 'datetime', date, url));
|
||||
}
|
||||
}
|
||||
|
||||
private getDisplayName(element: HTMLElement | Document, selector: string): string | null {
|
||||
const displayNameEl = element.querySelector<HTMLElement>(selector);
|
||||
return displayNameEl ? displayNameEl.innerText.trim() : null;
|
||||
}
|
||||
|
||||
private getUsername(element: HTMLElement | Document, selector: string): string | null {
|
||||
const usernameEl = element.querySelector<HTMLElement>(selector);
|
||||
return usernameEl ? usernameEl.innerText.replace('@', '').replace("/", '').trim() : null;
|
||||
}
|
||||
|
||||
private getContent(element: HTMLElement | Document, contentSelector: string, mediaSelector?: string): string | null {
|
||||
let content = '';
|
||||
|
||||
const contentEl = element.querySelector<HTMLElement>(contentSelector);
|
||||
if (contentEl && contentEl.textContent) {
|
||||
content = contentEl.textContent.trim();
|
||||
}
|
||||
|
||||
const mediaItems: string[] = [];
|
||||
|
||||
if (this.isVideoPage()) {
|
||||
const videoSrc = this.getVideoSrc(element, ELEMENT_SELECTORS.postVideo.videoUrl);
|
||||
if (videoSrc) {
|
||||
if (videoSrc.startsWith('blob:')) {
|
||||
mediaItems.push(`[video-blob](${videoSrc})`);
|
||||
} else {
|
||||
mediaItems.push(`[videoUrl](${videoSrc})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isPhotoPage()) {
|
||||
const photoUrls = this.getPhotoUrls(element, ELEMENT_SELECTORS.postPhoto.photoUrl);
|
||||
photoUrls.forEach((photoUrl, index) => {
|
||||
mediaItems.push(`[photo](${photoUrl})`);
|
||||
});
|
||||
}
|
||||
|
||||
let fullContent = '';
|
||||
|
||||
if (mediaItems.length > 0 && content) {
|
||||
const mediaLine = mediaItems.join(' ');
|
||||
fullContent = mediaLine + '\n\n' + content;
|
||||
} else if (mediaItems.length > 0) {
|
||||
fullContent = mediaItems.join(' ');
|
||||
} else if (content) {
|
||||
fullContent = turndownService.turndown(content);
|
||||
}
|
||||
|
||||
return fullContent || null;
|
||||
}
|
||||
|
||||
private getPhotoUrls(element: HTMLElement | Document, photoSelector: string): string[] {
|
||||
const photoUrls: string[] = [];
|
||||
const uniqueUrls = new Set<string>();
|
||||
|
||||
const photoContainer = element.querySelector<HTMLElement>(photoSelector);
|
||||
if (photoContainer) {
|
||||
const images = photoContainer.querySelectorAll('img');
|
||||
images.forEach((img) => {
|
||||
const src = img.src || img.getAttribute('data-src') || img.getAttribute('srcset')?.split(' ')[0] || '';
|
||||
if (src && !uniqueUrls.has(src)) {
|
||||
uniqueUrls.add(src);
|
||||
photoUrls.push(src);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return photoUrls;
|
||||
}
|
||||
|
||||
private getVideoSrc(element: HTMLElement | Document, videoSelector: string): string | null {
|
||||
const videoEl = element.querySelector<HTMLVideoElement>(videoSelector);
|
||||
if (videoEl && videoEl.src) {
|
||||
return videoEl.src;
|
||||
}
|
||||
|
||||
const anyVideoEl = element.querySelector<HTMLVideoElement>('video[src]');
|
||||
if (anyVideoEl && anyVideoEl.src) {
|
||||
return anyVideoEl.src;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private getInteractionCount(element: HTMLElement | Document, selector: string): string | null {
|
||||
const interactionEl = element.querySelector<HTMLElement>(selector);
|
||||
if (!interactionEl) return null;
|
||||
|
||||
const textContent = interactionEl.textContent;
|
||||
if (!textContent) return null;
|
||||
|
||||
const count = parseCount(textContent.replace(/\D/g, ''));
|
||||
return count || '0';
|
||||
}
|
||||
|
||||
|
||||
|
||||
addClassTagged(element: HTMLElement): void {
|
||||
element.classList.add('tagged');
|
||||
}
|
||||
|
||||
addClassSaved(element: HTMLElement): void {
|
||||
element.classList.add('saved');
|
||||
}
|
||||
}
|
||||
721
src/modules/x.ts
Normal file
721
src/modules/x.ts
Normal file
@@ -0,0 +1,721 @@
|
||||
import * as model from '../model';
|
||||
import { NewFact } from '../model';
|
||||
import { turndownService } from '../utils/turndown';
|
||||
|
||||
const ELEMENT_SELECTORS = {
|
||||
profile: {
|
||||
ld: 'script[type="application/ld+json"]'
|
||||
},
|
||||
post: {
|
||||
container: '[data-testid="tweet"]',
|
||||
showMore: 'button[data-testid="tweet-text-show-more-link"]',
|
||||
url: '[data-testid="tweet"] a[href*="status"]:first-of-type',
|
||||
profileImage: 'img[src*="profile_images"]',
|
||||
content: "[data-testid='tweetText']",
|
||||
imageUrl: "[data-testid='tweetPhoto'] img",
|
||||
videoUrl: "[data-testid='tweetPhoto'] video",
|
||||
like: "button[data-testid='like']",
|
||||
share: "button[data-testid='retweet']",
|
||||
comment: "button[data-testid='reply']",
|
||||
bookmark: "button[data-testid='bookmark']",
|
||||
views: "a[aria-label*='views']",
|
||||
date: "article time",
|
||||
displayName: "[data-testid='User-Name'] > div > div > a > div > div span",
|
||||
username: "[data-testid='User-Name'] > div:nth-child(2) > div > div > a > div span",
|
||||
},
|
||||
comment: {
|
||||
container: '[data-testid="tweet"]',
|
||||
url: '[data-testid="tweet"] a[href*="status"]:first-of-type',
|
||||
content: "[data-testid='tweetText']",
|
||||
profileImage: 'img[src*="profile_images"]',
|
||||
imageUrl: "[data-testid='tweetPhoto'] img",
|
||||
videoUrl: "[data-testid='tweetPhoto'] video",
|
||||
like: "button[data-testid='like']",
|
||||
share: "button[data-testid='retweet']",
|
||||
commentCount: "button[data-testid='reply']",
|
||||
bookmark: "button[data-testid='bookmark']",
|
||||
views: "a[aria-label*='views']",
|
||||
date: "article time",
|
||||
displayName: "[data-testid='User-Name'] > div > div > a > div > div span",
|
||||
username: "[data-testid='User-Name'] > div:nth-child(2) > div > div > a > div span",
|
||||
}
|
||||
} as const;
|
||||
|
||||
export default class XModule {
|
||||
context: string = "";
|
||||
|
||||
tagElement(): void {
|
||||
console.log(window.location.host);
|
||||
|
||||
Object.keys(ELEMENT_SELECTORS).forEach((type) => {
|
||||
const selectorObj = ELEMENT_SELECTORS[type as keyof typeof ELEMENT_SELECTORS];
|
||||
Object.entries(selectorObj).forEach(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(sel => {
|
||||
document.querySelectorAll<HTMLElement>(sel).forEach(element => this.addClassTagged(element));
|
||||
});
|
||||
} else if (key === "showMore") {
|
||||
const showMoreButton = document.querySelectorAll<HTMLElement>(value);
|
||||
showMoreButton.forEach(button => {
|
||||
this.addClassTagged(button);
|
||||
button.click();
|
||||
});
|
||||
} else {
|
||||
document.querySelectorAll<HTMLElement>(value).forEach(element => this.addClassTagged(element));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async saveData(): Promise<model.FactCollection[]> {
|
||||
const factCollections: model.FactCollection[] = [];
|
||||
|
||||
try {
|
||||
if (this.isStatusPage()) {
|
||||
await this.handleStatusPage(factCollections);
|
||||
} else {
|
||||
await this.handleProfilePage(factCollections);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error in saveData:", error);
|
||||
}
|
||||
|
||||
return factCollections;
|
||||
}
|
||||
|
||||
private isStatusPage(): boolean {
|
||||
return window.location.href.includes('/status/');
|
||||
}
|
||||
|
||||
private async handleProfilePage(factCollections: model.FactCollection[]): Promise<void> {
|
||||
const profile = this.saveProfile();
|
||||
if (profile.facts.length > 0) {
|
||||
factCollections.push(profile);
|
||||
this.markElementSaved(ELEMENT_SELECTORS.profile.ld);
|
||||
}
|
||||
|
||||
// Expand all show more buttons and wait for content to load
|
||||
await this.expandAllPostContent();
|
||||
|
||||
// Then save the posts with expanded content
|
||||
const posts = await this.savePost();
|
||||
if (posts.facts.length > 0) {
|
||||
factCollections.push(posts);
|
||||
this.markElementSaved(ELEMENT_SELECTORS.post.container);
|
||||
}
|
||||
}
|
||||
|
||||
private async expandAllPostContent(): Promise<void> {
|
||||
const showMoreButtons = document.querySelectorAll<HTMLElement>(ELEMENT_SELECTORS.post.showMore);
|
||||
|
||||
if (showMoreButtons.length === 0) {
|
||||
console.log("No show more buttons found");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Found ${showMoreButtons.length} show more buttons to expand`);
|
||||
|
||||
// Click all buttons first
|
||||
showMoreButtons.forEach(button => {
|
||||
this.addClassTagged(button);
|
||||
button.click();
|
||||
});
|
||||
|
||||
// Wait for all content to expand
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
console.log("All post content expansion completed");
|
||||
}
|
||||
|
||||
private async handleStatusPage(factCollections: model.FactCollection[]): Promise<void> {
|
||||
// Expand all show more buttons first
|
||||
await this.expandAllPostContent();
|
||||
|
||||
const posts = await this.savePost();
|
||||
if (posts.facts.length > 0) {
|
||||
factCollections.push(posts);
|
||||
this.markElementSaved(ELEMENT_SELECTORS.post.container);
|
||||
}
|
||||
|
||||
const comments = await this.saveComment();
|
||||
if (comments.facts.length > 0) {
|
||||
factCollections.push(comments);
|
||||
}
|
||||
}
|
||||
|
||||
private markElementSaved(selector: string): void {
|
||||
const element = document.querySelector<HTMLElement>(selector) || document.body;
|
||||
this.addClassSaved(element);
|
||||
}
|
||||
|
||||
saveProfile(): model.FactCollection {
|
||||
const collection: model.FactCollection = model.NewFactCollection('profile');
|
||||
collection.url = window.location.href;
|
||||
|
||||
|
||||
const ldScript = document.querySelector<HTMLScriptElement>(`${ELEMENT_SELECTORS.profile.ld}:not(.saved)`);
|
||||
|
||||
if (!ldScript || !ldScript.textContent) {
|
||||
return collection;
|
||||
}
|
||||
try {
|
||||
const jsonData = JSON.parse(ldScript.textContent);
|
||||
if (jsonData["@type"] === "ProfilePage" && jsonData.mainEntity && !ldScript.classList.contains('saved')) {
|
||||
this.extractProfileFacts(jsonData.mainEntity, collection);
|
||||
this.addClassSaved(ldScript);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing LD+JSON:", error);
|
||||
}
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
private extractProfileFacts(mainEntity: any, collection: model.FactCollection): void {
|
||||
const contextUrl = collection.url;
|
||||
|
||||
collection.facts.push(NewFact('x_profile_source_url', 'url', contextUrl, contextUrl));
|
||||
|
||||
const username = mainEntity.additionalName || this.extractUsernameFromUrl(mainEntity.url) || '';
|
||||
if (username) {
|
||||
collection.facts.push(NewFact('x_profile_user_name', 'name', username, contextUrl));
|
||||
}
|
||||
|
||||
const displayName = mainEntity.givenName || mainEntity.name || '';
|
||||
if (displayName) {
|
||||
collection.facts.push(NewFact('x_profile_display_name', 'name', displayName, contextUrl));
|
||||
}
|
||||
|
||||
const description = mainEntity.description || '';
|
||||
if (description) {
|
||||
collection.facts.push(NewFact('x_profile_bio_md', 'md', description, contextUrl));
|
||||
}
|
||||
|
||||
this.extractProfileImage(mainEntity.image, collection, contextUrl);
|
||||
|
||||
if (mainEntity.interactionStatistic && Array.isArray(mainEntity.interactionStatistic)) {
|
||||
this.extractInteractionStatistics(mainEntity.interactionStatistic, collection, contextUrl);
|
||||
}
|
||||
}
|
||||
|
||||
private extractProfileImage(image: any, collection: model.FactCollection, contextUrl: string): void {
|
||||
if (!image) return;
|
||||
|
||||
let imageUrl = '';
|
||||
if (typeof image === "string") {
|
||||
imageUrl = image;
|
||||
} else if (typeof image === "object" && image.contentUrl) {
|
||||
imageUrl = image.contentUrl;
|
||||
}
|
||||
|
||||
if (imageUrl) {
|
||||
collection.facts.push(NewFact('x_profile_profileimage_url', 'url', imageUrl, contextUrl));
|
||||
}
|
||||
}
|
||||
|
||||
private extractInteractionStatistics(statistics: any[], collection: model.FactCollection, contextUrl: string): void {
|
||||
statistics.forEach((stat: any) => {
|
||||
const count = stat.userInteractionCount?.toString() || '0';
|
||||
|
||||
if (stat.interactionType === "https://schema.org/FollowAction" && stat.name === "Follows") {
|
||||
collection.facts.push(NewFact('x_profile_follower_count', 'number', count, contextUrl));
|
||||
} else if (stat.interactionType === "https://schema.org/SubscribeAction" && stat.name === "Friends") {
|
||||
collection.facts.push(NewFact('x_profile_following_count', 'number', count, contextUrl));
|
||||
} else if (stat.interactionType === "https://schema.org/WriteAction" && stat.name === "Tweets") {
|
||||
collection.facts.push(NewFact('x_profile_post_count', 'number', count, contextUrl));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async savePost(): Promise<model.FactCollection> {
|
||||
const posts: model.FactCollection = model.NewFactCollection('post');
|
||||
const selectors = ELEMENT_SELECTORS.post;
|
||||
|
||||
|
||||
if (this.isStatusPage()) {
|
||||
const conversationTimeline = document.querySelector<HTMLElement>('[aria-label="Timeline: Conversation"]:not(.saved)');
|
||||
console.log('Conversation Timeline:', conversationTimeline);
|
||||
if (conversationTimeline) {
|
||||
const postElements = conversationTimeline.querySelector<HTMLElement>(`${selectors.container}:not(.saved)`);
|
||||
await this.extractFromPostElement(postElements, posts, selectors);
|
||||
this.addClassSaved(conversationTimeline);
|
||||
} else {
|
||||
console.warn('No conversation timeline found on status page.');
|
||||
}
|
||||
} else {
|
||||
const postElements = document.querySelectorAll<HTMLElement>(`${selectors.container}:not(.saved)`);
|
||||
postElements.forEach(element => this.addClassTagged(element));
|
||||
await this.extractFromPostElements(postElements, posts, selectors);
|
||||
}
|
||||
|
||||
return posts;
|
||||
}
|
||||
|
||||
private async extractFromPostElements(
|
||||
postElements: NodeListOf<HTMLElement> | HTMLElement[],
|
||||
posts: model.FactCollection,
|
||||
selectors: typeof ELEMENT_SELECTORS.post
|
||||
): Promise<void> {
|
||||
await Promise.all(Array.from(postElements).map(async (element) => {
|
||||
try {
|
||||
const urlEl = element.querySelector<HTMLElement>(selectors.url);
|
||||
if (!urlEl) return;
|
||||
|
||||
const href = urlEl.getAttribute('href');
|
||||
if (!href) return;
|
||||
|
||||
const fullUrl = href.startsWith('http') ? href : `https://x.com${href}`;
|
||||
|
||||
const facts = this.createFactsFromExtractedData(element, selectors, fullUrl);
|
||||
posts.facts.push(...facts);
|
||||
this.addClassSaved(element);
|
||||
} catch (error) {
|
||||
console.error('Error processing post element:', error);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private async extractFromPostElement(
|
||||
postElement: HTMLElement | null,
|
||||
posts: model.FactCollection,
|
||||
selectors: typeof ELEMENT_SELECTORS.post
|
||||
): Promise<void> {
|
||||
if (!postElement) return;
|
||||
|
||||
try {
|
||||
const urlEl = postElement.querySelector<HTMLElement>(selectors.url);
|
||||
if (!urlEl) return;
|
||||
|
||||
const href = urlEl.getAttribute('href');
|
||||
if (!href) return;
|
||||
|
||||
const fullUrl = href.startsWith('http') ? href : `https://x.com${href}`;
|
||||
|
||||
const facts = this.createFactsFromExtractedData(postElement, selectors, fullUrl);
|
||||
posts.facts.push(...facts);
|
||||
this.addClassSaved(postElement);
|
||||
} catch (error) {
|
||||
console.error('Error processing post element:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private createFactsFromExtractedData(
|
||||
element: HTMLElement,
|
||||
selectors: typeof ELEMENT_SELECTORS.post,
|
||||
fullUrl: string
|
||||
): model.Fact[] {
|
||||
const facts: model.Fact[] = [];
|
||||
|
||||
const contextUrl = fullUrl;
|
||||
facts.push(NewFact('x_post_source_url', 'url', fullUrl, contextUrl));
|
||||
|
||||
const postData = this.extractPostData(element, selectors);
|
||||
|
||||
if (postData.profileUrl) facts.push(NewFact('x_post_profile_url', 'url', postData.profileUrl, contextUrl));
|
||||
|
||||
if (postData.displayName) facts.push(NewFact('x_post_display_name', 'name', postData.displayName, contextUrl));
|
||||
if (postData.username) facts.push(NewFact('x_post_user_name', 'name', postData.username, contextUrl));
|
||||
if (postData.profileImage) facts.push(NewFact('x_post_profileimage_url', 'url', postData.profileImage, contextUrl));
|
||||
if (postData.content) facts.push(NewFact('x_post_content_md', 'md', postData.content, contextUrl));
|
||||
if (postData.likeCount !== null) facts.push(NewFact('x_post_like_count', 'number', postData.likeCount, contextUrl));
|
||||
if (postData.shareCount !== null) facts.push(NewFact('x_post_share_count', 'number', postData.shareCount, contextUrl));
|
||||
if (postData.commentCount !== null) facts.push(NewFact('x_post_comment_count', 'number', postData.commentCount, contextUrl));
|
||||
if (postData.bookmarkCount !== null) facts.push(NewFact('x_post_bookmark_count', 'number', postData.bookmarkCount, contextUrl));
|
||||
if (postData.viewCount !== null) facts.push(NewFact('x_post_view_count', 'number', postData.viewCount, contextUrl));
|
||||
if (postData.date) facts.push(NewFact('x_post_createdat_datetime', 'datetime', postData.date, contextUrl));
|
||||
|
||||
return facts;
|
||||
}
|
||||
|
||||
private extractPostData(element: HTMLElement, selectors: typeof ELEMENT_SELECTORS.post): {
|
||||
displayName: string | null;
|
||||
profileUrl: string | null;
|
||||
username: string | null;
|
||||
profileImage: string | null;
|
||||
content: string | null;
|
||||
likeCount: string | null;
|
||||
shareCount: string | null;
|
||||
commentCount: string | null;
|
||||
bookmarkCount: string | null;
|
||||
viewCount: string | null;
|
||||
date: string | null;
|
||||
} {
|
||||
return {
|
||||
displayName: this.getDisplayName(element, selectors.displayName),
|
||||
username: this.getUsername(element, selectors.username),
|
||||
profileUrl: `https://x.com${element.querySelector<HTMLElement>('div[data-testid="User-Name"] a')?.getAttribute('href')}` || null,
|
||||
profileImage: this.getProfileImage(element, selectors.profileImage),
|
||||
content: this.getContent(element, {
|
||||
content: selectors.content,
|
||||
imageUrl: selectors.imageUrl,
|
||||
videoUrl: selectors.videoUrl,
|
||||
}),
|
||||
likeCount: this.getInteractionCount(element, selectors.like),
|
||||
shareCount: this.getInteractionCount(element, selectors.share),
|
||||
commentCount: this.getInteractionCount(element, selectors.comment),
|
||||
bookmarkCount: this.getBookmarkCount(element, selectors.bookmark),
|
||||
viewCount: this.getViewCount(element, selectors.views),
|
||||
date: this.getDate(element, selectors.date),
|
||||
};
|
||||
}
|
||||
|
||||
async saveComment(): Promise<model.FactCollection> {
|
||||
const comments: model.FactCollection = model.NewFactCollection('comment');
|
||||
|
||||
if (!this.isStatusPage()) {
|
||||
return comments;
|
||||
}
|
||||
|
||||
const selectors = ELEMENT_SELECTORS.comment;
|
||||
let commentElements: HTMLElement[] = [];
|
||||
|
||||
const conversationTimeline = document.querySelector('[aria-label="Timeline: Conversation"]');
|
||||
|
||||
if (conversationTimeline) {
|
||||
const allArticlesInConversation = conversationTimeline.querySelectorAll<HTMLElement>('[data-testid="tweet"]');
|
||||
const commentsInConversation = Array.from(allArticlesInConversation).slice(1).filter(element => {
|
||||
return !element.classList.contains('saved');
|
||||
});
|
||||
commentElements.push(...commentsInConversation);
|
||||
console.log(`Found ${allArticlesInConversation.length} total articles in conversation, ${commentsInConversation.length} comments in conversation`);
|
||||
}
|
||||
|
||||
const allTweetElements = document.querySelectorAll<HTMLElement>(`${selectors.container}:not(.saved)`);
|
||||
const outsideCommentElements = Array.from(allTweetElements).filter(element => {
|
||||
const conversationContainer = element.closest('[aria-label="Timeline: Conversation"]');
|
||||
return conversationContainer === null;
|
||||
});
|
||||
|
||||
commentElements.push(...outsideCommentElements);
|
||||
commentElements.forEach(element => this.addClassTagged(element));
|
||||
|
||||
await Promise.all(commentElements.map(async (element) => {
|
||||
try {
|
||||
const urlEl = element.querySelector<HTMLElement>(selectors.url);
|
||||
if (!urlEl) return;
|
||||
|
||||
const href = urlEl.getAttribute('href');
|
||||
if (!href) return;
|
||||
|
||||
const fullUrl = href.startsWith('http') ? href : `https://x.com${href}`;
|
||||
const pageContext = window.location.href; // context halaman status/thread
|
||||
|
||||
const facts = this.createCommentFactsFromExtractedData(element, selectors, fullUrl, pageContext);
|
||||
if (facts?.length > 0) {
|
||||
comments.facts.push(...facts);
|
||||
}
|
||||
|
||||
this.addClassSaved(element);
|
||||
} catch (error) {
|
||||
console.error('Error processing comment element:', error);
|
||||
}
|
||||
}));
|
||||
|
||||
return comments;
|
||||
}
|
||||
|
||||
private createCommentFactsFromExtractedData(
|
||||
element: HTMLElement,
|
||||
selectors: typeof ELEMENT_SELECTORS.comment,
|
||||
fullUrl: string,
|
||||
pageContext: string
|
||||
): model.Fact[] {
|
||||
const facts: model.Fact[] = [];
|
||||
|
||||
const commentData = this.extractCommentData(element, selectors);
|
||||
if (!commentData.date) return facts;
|
||||
|
||||
// comment URL dengan context = halaman saat ini
|
||||
facts.push(NewFact('x_comment_source_url', 'url', fullUrl, fullUrl));
|
||||
// fakta lain memakai context = URL komentar itu sendiri
|
||||
const contextUrl = fullUrl;
|
||||
if (commentData.profileUrl) facts.push(NewFact('x_comment_profile_url', 'url', commentData.profileUrl, contextUrl));
|
||||
if (commentData.date) facts.push(NewFact('x_comment_createdat_datetime', 'datetime', commentData.date, contextUrl));
|
||||
if (commentData.displayName) facts.push(NewFact('x_comment_display_name', 'name', commentData.displayName, contextUrl));
|
||||
if (commentData.username) facts.push(NewFact('x_comment_user_name', 'name', commentData.username, contextUrl));
|
||||
if (commentData.profileImage) facts.push(NewFact('x_comment_profileimage_url', 'url', commentData.profileImage, contextUrl));
|
||||
if (commentData.content) facts.push(NewFact('x_comment_content_md', 'md', commentData.content, contextUrl));
|
||||
if (commentData.likeCount !== null) facts.push(NewFact('x_comment_like_count', 'number', commentData.likeCount, contextUrl));
|
||||
if (commentData.shareCount !== null) facts.push(NewFact('x_comment_share_count', 'number', commentData.shareCount, contextUrl));
|
||||
if (commentData.commentCount !== null) facts.push(NewFact('x_comment_comment_count', 'number', commentData.commentCount, contextUrl));
|
||||
if (commentData.bookmarkCount !== null) facts.push(NewFact('x_comment_bookmark_count', 'number', commentData.bookmarkCount, contextUrl));
|
||||
if (commentData.viewCount !== null) facts.push(NewFact('x_comment_view_count', 'number', commentData.viewCount, contextUrl));
|
||||
|
||||
return facts;
|
||||
}
|
||||
|
||||
private extractCommentData(element: HTMLElement, selectors: typeof ELEMENT_SELECTORS.comment): {
|
||||
displayName: string | null;
|
||||
username: string | null;
|
||||
profileImage: string | null;
|
||||
profileUrl: string | null;
|
||||
content: string | null;
|
||||
likeCount: string | null;
|
||||
shareCount: string | null;
|
||||
commentCount: string | null;
|
||||
bookmarkCount: string | null;
|
||||
viewCount: string | null;
|
||||
date: string | null;
|
||||
} {
|
||||
return {
|
||||
displayName: this.getDisplayName(element, selectors.displayName),
|
||||
username: this.getUsername(element, selectors.username),
|
||||
profileUrl: `https://x.com${element.querySelector<HTMLElement>('div[data-testid="User-Name"] a')?.getAttribute('href')}` || null,
|
||||
profileImage: this.getProfileImage(element, selectors.profileImage),
|
||||
content: this.getContent(element, {
|
||||
content: selectors.content,
|
||||
imageUrl: selectors.imageUrl,
|
||||
videoUrl: selectors.videoUrl,
|
||||
}),
|
||||
likeCount: this.getInteractionCount(element, selectors.like),
|
||||
shareCount: this.getInteractionCount(element, selectors.share),
|
||||
commentCount: this.getInteractionCount(element, selectors.commentCount),
|
||||
viewCount: this.getViewCount(element, selectors.views),
|
||||
bookmarkCount: this.getBookmarkCount(element, selectors.bookmark),
|
||||
date: this.getDate(element, selectors.date),
|
||||
};
|
||||
}
|
||||
|
||||
private getDisplayName(element: HTMLElement, selector: string): string | null {
|
||||
const displayNameEl = element.querySelector<HTMLElement>(selector);
|
||||
return displayNameEl ? displayNameEl.innerText.trim() : null;
|
||||
}
|
||||
|
||||
private getUsername(element: HTMLElement, selector: string): string | null {
|
||||
const usernameEl = element.querySelector<HTMLElement>(selector);
|
||||
return usernameEl ? usernameEl.innerText.replace('@', '').trim() : null;
|
||||
}
|
||||
|
||||
private getBookmarkCount(element: HTMLElement, selector: string): string | null {
|
||||
const bookmarkEl = element.querySelector<HTMLElement>("[aria-label*='bookmark']");
|
||||
if (!bookmarkEl) return null;
|
||||
|
||||
const ariaLabel = bookmarkEl.getAttribute('aria-label');
|
||||
if (!ariaLabel) return null;
|
||||
|
||||
const parts = ariaLabel.split(', ');
|
||||
const bookmarkPart = parts.find(part => part.includes('bookmark'));
|
||||
|
||||
if (bookmarkPart) {
|
||||
const bookmarkValue = bookmarkPart.trim().split(' ')[0];
|
||||
const count = parseInt(bookmarkValue || '0');
|
||||
return isNaN(count) ? '0' : count.toString();
|
||||
}
|
||||
|
||||
return '0';
|
||||
}
|
||||
|
||||
private buildFinalContent(
|
||||
media: { type: 'photo' | 'video'; url: string }[],
|
||||
text: string | null
|
||||
): string | null {
|
||||
const lines: string[] = [];
|
||||
|
||||
media.forEach(({ type, url }) => {
|
||||
lines.push(`[${type}Url](${url})`);
|
||||
});
|
||||
|
||||
const trimmedText = text?.trim();
|
||||
if (trimmedText) {
|
||||
if (lines.length) lines.push('');
|
||||
lines.push(trimmedText);
|
||||
}
|
||||
|
||||
return lines.join('\n').trim() || null;
|
||||
}
|
||||
|
||||
private getContent(
|
||||
element: HTMLElement,
|
||||
selectors: Pick<typeof ELEMENT_SELECTORS.post, 'content' | 'imageUrl' | 'videoUrl'>
|
||||
): string | null {
|
||||
const lines: string[] = [];
|
||||
|
||||
const photoEls = element.querySelectorAll<HTMLImageElement>(selectors.imageUrl);
|
||||
photoEls.forEach(img => lines.push(`[photoUrl](${img.src})`));
|
||||
|
||||
const videoEls = element.querySelectorAll<HTMLVideoElement>(selectors.videoUrl);
|
||||
videoEls.forEach(v => {
|
||||
if (v.poster) lines.push(`[videoUrl](${v.poster})`);
|
||||
const src = v.querySelector('source')?.src || v.src;
|
||||
if (src && !src.startsWith('blob:')) lines.push(`[videoUrl](${src})`);
|
||||
});
|
||||
|
||||
const textEl = element.querySelector<HTMLElement>(selectors.content);
|
||||
const text = textEl ? turndownService.turndown(textEl.innerHTML).trim() : null;
|
||||
|
||||
if (text) {
|
||||
if (lines.length) lines.push('');
|
||||
lines.push(text);
|
||||
}
|
||||
|
||||
return lines.join('\n').trim() || null;
|
||||
}
|
||||
|
||||
private hasVideoIndicator(element: HTMLElement): boolean {
|
||||
const videoIndicators = [
|
||||
'[data-testid="videoPlayer"]',
|
||||
'[data-testid="videoComponent"]',
|
||||
'[data-testid="videoControls"]'
|
||||
];
|
||||
|
||||
for (const selector of videoIndicators) {
|
||||
if (element.querySelector(selector)) {
|
||||
console.log(`Found video indicator: ${selector}`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const tweetPhoto = element.querySelector('[data-testid="tweetPhoto"]');
|
||||
if (tweetPhoto) {
|
||||
const playElements = [
|
||||
'svg[aria-label*="Play" i]',
|
||||
'button[aria-label*="Play" i]',
|
||||
'button[aria-label*="Pause" i]',
|
||||
'button[aria-label*="Watch again" i]',
|
||||
'[aria-label*="Play video" i]'
|
||||
];
|
||||
|
||||
for (const selector of playElements) {
|
||||
const playElement = tweetPhoto.querySelector(selector);
|
||||
if (playElement) {
|
||||
console.log(`Found video play element: ${selector}`, playElement);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const videoElement = tweetPhoto.querySelector('video');
|
||||
if (videoElement) {
|
||||
console.log('Found video element:', videoElement);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('No video indicators found - treating as image content');
|
||||
return false;
|
||||
}
|
||||
|
||||
getProfileImage(element: HTMLElement, selector: string): string {
|
||||
const imgEl = element.querySelector<HTMLImageElement>(selector);
|
||||
if (!imgEl) return '';
|
||||
const imgSrc = imgEl.src.trim();
|
||||
return imgSrc || '';
|
||||
}
|
||||
|
||||
private isVideoThumbnail(img: HTMLImageElement): boolean {
|
||||
const parent = img.closest('[data-testid="tweetPhoto"]');
|
||||
if (!parent) return false;
|
||||
|
||||
const hasVideoIndicators = parent.querySelector('[aria-label*="Play"]') ||
|
||||
parent.querySelector('svg') ||
|
||||
parent.querySelector('[role="button"]');
|
||||
|
||||
const videoThumbnailPatterns = [
|
||||
'amplify_video_thumb',
|
||||
'video_thumb',
|
||||
'ext_tw_video_thumb',
|
||||
'thumbnail',
|
||||
'poster',
|
||||
'preview'
|
||||
];
|
||||
|
||||
const imgSrc = img.src.toLowerCase();
|
||||
const hasVideoPattern = videoThumbnailPatterns.some(pattern => imgSrc.includes(pattern));
|
||||
|
||||
return !!(hasVideoIndicators || hasVideoPattern);
|
||||
}
|
||||
|
||||
private hasStrongVideoIndicators(img: HTMLImageElement): boolean {
|
||||
const parent = img.closest('[data-testid="tweetPhoto"]');
|
||||
if (!parent) return false;
|
||||
|
||||
const hasExplicitPlayButton = parent.querySelector('svg[aria-label*="Play video" i]') ||
|
||||
parent.querySelector('button[aria-label*="Play video" i]') ||
|
||||
parent.querySelector('[data-testid="videoPlayer"]') ||
|
||||
parent.querySelector('[data-testid="videoComponent"]');
|
||||
|
||||
const hasVideoElement = parent.querySelector('video');
|
||||
const result = !!(hasExplicitPlayButton || hasVideoElement);
|
||||
|
||||
if (result) {
|
||||
console.log('Image has STRONG video indicators:', {
|
||||
hasExplicitPlayButton: !!hasExplicitPlayButton,
|
||||
hasVideoElement: !!hasVideoElement,
|
||||
imgSrc: img.src
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private getInteractionCount(element: HTMLElement, selector: string): string | null {
|
||||
const interactionEl = element.querySelector<HTMLElement>(selector);
|
||||
if (!interactionEl) return null;
|
||||
|
||||
const ariaLabel = interactionEl.getAttribute('aria-label');
|
||||
if (!ariaLabel) return null;
|
||||
|
||||
const countStr = ariaLabel.split(' ')[0];
|
||||
const count = parseInt(countStr || '0');
|
||||
return isNaN(count) ? '0' : count.toString();
|
||||
}
|
||||
|
||||
private getViewCount(element: HTMLElement, selector: string): string | null {
|
||||
let viewEl = element.querySelector<HTMLElement>(selector);
|
||||
|
||||
if (viewEl && viewEl.getAttribute('aria-label')) {
|
||||
const ariaLabel = viewEl.getAttribute('aria-label');
|
||||
const viewValue = ariaLabel?.split(' ')[0] || '0';
|
||||
return viewValue;
|
||||
}
|
||||
|
||||
viewEl = element.querySelector<HTMLElement>("[aria-label*='views']");
|
||||
if (viewEl) {
|
||||
const ariaLabel = viewEl.getAttribute('aria-label');
|
||||
if (ariaLabel) {
|
||||
const parts = ariaLabel.split(', ');
|
||||
const viewsPart = parts.find(part => part.includes('views'));
|
||||
if (viewsPart) {
|
||||
const viewValue = viewsPart.trim().split(' ')[0];
|
||||
return viewValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewEl = element.querySelector<HTMLElement>("span[data-testid='app-text-transition-container']");
|
||||
if (viewEl && viewEl.textContent) {
|
||||
return viewEl.textContent.trim();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private getDate(element: HTMLElement, selector: string): string | null {
|
||||
const dateEl = element.querySelector<HTMLElement>(selector);
|
||||
return dateEl ? dateEl.getAttribute('datetime') || null : null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
extractUsernameFromUrl(url: string): string | null {
|
||||
const match = url?.match(/x\.com\/([^\/]+)$/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
addClassTagged(element: HTMLElement): void {
|
||||
element.classList.add('x-tagged');
|
||||
}
|
||||
|
||||
addClassSaved(element: HTMLElement): void {
|
||||
element.classList.add('saved');
|
||||
}
|
||||
|
||||
isTagged(element: HTMLElement): boolean {
|
||||
return element.classList.contains('x-tagged');
|
||||
}
|
||||
|
||||
isSaved(element: HTMLElement): boolean {
|
||||
return element.classList.contains('saved');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
1000
src/modules/youtube.ts
Normal file
1000
src/modules/youtube.ts
Normal file
File diff suppressed because it is too large
Load Diff
128
src/popup/popup.css
Normal file
128
src/popup/popup.css
Normal file
@@ -0,0 +1,128 @@
|
||||
:root {
|
||||
--primary-color: #333;
|
||||
--background-color: #f8f9fa;
|
||||
--text-color: #333;
|
||||
--border-color: #e0e0e0;
|
||||
--button-hover-color: #333;
|
||||
--panel-width: 20rem;
|
||||
}
|
||||
|
||||
body {
|
||||
width: var(--panel-width);
|
||||
max-width: var(--panel-width);
|
||||
min-width: var(--panel-width);
|
||||
min-height: 100vh;
|
||||
background: var(--background-color);
|
||||
color: var(--text-color);
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
justify-content: flex-start;
|
||||
gap: 0.625rem;
|
||||
box-sizing: border-box;
|
||||
height: 100vh;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.625rem 1.25rem;
|
||||
border-radius: 0.313rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
margin: 0.625rem 0;
|
||||
width: 100%;
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: var(--button-hover-color);
|
||||
}
|
||||
|
||||
.btn-menu {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
justify-content: end;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#output {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow-x: hidden;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#output pre {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
max-height: 18.75rem;
|
||||
font-size: 0.625rem;
|
||||
line-height: 1.2;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.menu-container {
|
||||
position: absolute;
|
||||
height: auto;
|
||||
width: 8.125rem;
|
||||
right: 0.625rem;
|
||||
display: none;
|
||||
top: 3.75rem;
|
||||
background-color: white;
|
||||
border: 0.063rem solid #ccc;
|
||||
padding: 0.625rem;
|
||||
justify-content: left;
|
||||
border-radius: 0.625rem;
|
||||
}
|
||||
|
||||
.menu-container>ul {
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
gap: 0.313rem;
|
||||
}
|
||||
|
||||
.menu-container>ul>li {
|
||||
gap: 1px;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 0.813rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.menu-container>ul>li>a:hover {
|
||||
background-color: #333333;
|
||||
color: #fff;
|
||||
|
||||
}
|
||||
|
||||
.menu-container>ul>li>a {
|
||||
text-decoration: none;
|
||||
color: #333;
|
||||
width: 100%;
|
||||
display: block;
|
||||
padding: 5px 5px;
|
||||
border-radius: 5px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.active {
|
||||
display: block;
|
||||
}
|
||||
51
src/popup/popup.html
Normal file
51
src/popup/popup.html
Normal file
@@ -0,0 +1,51 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Popup</title>
|
||||
<link rel="stylesheet" href="popup.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="header" style="display: flex; align-items: center; justify-content: space-between;">
|
||||
<button id="toggle-sidebar-btn" style="width: 10px; align-items: center; display: flex; justify-content: center;border-radius: 50%; color: #333; background-color: #f8f9fa;">x</button>
|
||||
<h2>New Browser Input</h1>
|
||||
<a class="btn-menu">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<!-- New toggle button -->
|
||||
<div class="menu-container">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="">Profile</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="">Settings</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="">Logout</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button id="tag-btn" data-action="tagElement">Tag</button>
|
||||
<button id="save-btn" data-action="saveData">Save Data</button>
|
||||
<form action="" id="form-api-endpoint" style="display: flex; justify-content: center; align-items: center; height: 50px; margin-top: 10px;">
|
||||
<input type="text" name="api-endpoint" id="field-api-endpoint" placeholder="Input Endpoint Api" id=""
|
||||
style="width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 4px; margin-right: 10px;" />
|
||||
<button type="submit" style=" font-size: 10px; width: 30%;">Save endpoint</button>
|
||||
</form>
|
||||
<hr>
|
||||
<h3>
|
||||
Output
|
||||
</h3>
|
||||
<div id="output"></div>
|
||||
<script type="module" src="popup.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
190
src/popup/popup.ts
Normal file
190
src/popup/popup.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
type PopupActionHandler = () => void;
|
||||
|
||||
const popupActionHandlers: Record<string, PopupActionHandler> = {
|
||||
tagElement: handleTagElement,
|
||||
saveData: handleSaveData
|
||||
}
|
||||
|
||||
// Main function to execute a popup action
|
||||
interface ExecuteActionResponse {
|
||||
success: boolean;
|
||||
data?: unknown;
|
||||
key?: string;
|
||||
}
|
||||
|
||||
function executeAction(action: keyof typeof popupActionHandlers): void {
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs: chrome.tabs.Tab[]) => {
|
||||
chrome.tabs.sendMessage(
|
||||
tabs[0].id as number,
|
||||
{ action },
|
||||
(response: ExecuteActionResponse) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error('Gagal mengirim pesan:', chrome.runtime.lastError.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response && response.success) {
|
||||
console.log('Aksi berhasil:', response);
|
||||
|
||||
// Tampilkan data otomatis setelah save
|
||||
if (action === 'saveData' && response.data) {
|
||||
displaySavedData(response.data, response.key);
|
||||
}
|
||||
} else {
|
||||
console.error('Aksi gagal:', response);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
interface DisplaySavedDataParams {
|
||||
data: unknown;
|
||||
key?: string;
|
||||
}
|
||||
|
||||
function displaySavedData(data: DisplaySavedDataParams['data'], key: DisplaySavedDataParams['key']): void {
|
||||
const outputElement: HTMLElement | null = document.getElementById('output');
|
||||
if (outputElement) {
|
||||
let htmlContent = `
|
||||
<div style="margin-top: 10px; padding: 10px; border: 1px solid #4CAF50; border-radius: 4px; background: #f9fff9; width: 100%; box-sizing: border-box;">
|
||||
<h3 style="color: #4CAF50; margin: 0 0 10px 0; font-size: 14px; word-wrap: break-word;">✅ Data Berhasil Disimpan</h3>
|
||||
<p style="margin: 5px 0; font-size: 11px; color: #666; word-wrap: break-word; overflow-wrap: break-word;"><strong>Key:</strong> ${key || 'N/A'}</p>
|
||||
`;
|
||||
|
||||
// Check if data is an array of fact collections
|
||||
if (Array.isArray(data)) {
|
||||
data.forEach((collection: any, index: number) => {
|
||||
htmlContent += `
|
||||
<div style="margin: 15px 0; border: 1px solid #ddd; border-radius: 4px; overflow: hidden;">
|
||||
<div style="background: #f0f0f0; padding: 8px; font-weight: bold; font-size: 12px;">
|
||||
${collection.type || 'Collection'} ${index + 1} - ${collection.url || 'N/A'}
|
||||
</div>
|
||||
<div style="padding: 5px;">
|
||||
<p style="margin: 5px 0; font-size: 10px; color: #666;">
|
||||
<strong>Saved:</strong> ${collection.savedAt ? new Date(collection.savedAt).toLocaleString() : 'N/A'}
|
||||
</p>
|
||||
<div style="overflow-x: auto; max-width: 100%; border: 1px solid #ddd; border-radius: 4px;">
|
||||
<table style="width: max-content; min-width: 100%; border-collapse: collapse; font-size: 10px; margin: 0;">
|
||||
<thead>
|
||||
<tr style="background: #f8f8f8;">
|
||||
<th style="border: 1px solid #ddd; padding: 6px; text-align: left; font-weight: bold; min-width: 100px; white-space: nowrap;">Name</th>
|
||||
<th style="border: 1px solid #ddd; padding: 6px; text-align: left; font-weight: bold; min-width: 80px; white-space: nowrap;">Type</th>
|
||||
<th style="border: 1px solid #ddd; padding: 6px; text-align: left; font-weight: bold; min-width: 300px; white-space: nowrap;">Value</th>
|
||||
<th style="border: 1px solid #ddd; padding: 6px; text-align: left; font-weight: bold; min-width: 200px; white-space: nowrap;">Context</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
|
||||
// Add facts to table
|
||||
if (collection.facts && Array.isArray(collection.facts)) {
|
||||
collection.facts.forEach((fact: any) => {
|
||||
const value = fact.value ? String(fact.value) : 'N/A';
|
||||
const context = fact.context || 'N/A';
|
||||
|
||||
htmlContent += `
|
||||
<tr>
|
||||
<td style="border: 1px solid #ddd; padding: 6px; word-break: break-word; white-space: pre-wrap;">${fact.name || 'N/A'}</td>
|
||||
<td style="border: 1px solid #ddd; padding: 6px; white-space: nowrap;">${fact.type || 'N/A'}</td>
|
||||
<td style="border: 1px solid #ddd; padding: 6px; word-break: break-word; white-space: pre-wrap; max-width: 400px;">${value}</td>
|
||||
<td style="border: 1px solid #ddd; padding: 6px; word-break: break-word; white-space: pre-wrap; max-width: 300px;">${context}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
htmlContent += `
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
} else {
|
||||
// Fallback for non-array data
|
||||
htmlContent += `
|
||||
<pre style="background: #f5f5f5; padding: 8px; border-radius: 4px; font-size: 10px; margin: 10px 0 0 0; white-space: pre-wrap; word-break: break-all; overflow-wrap: break-word; max-width: 100%; overflow-x: hidden; box-sizing: border-box; line-height: 1.4;">${JSON.stringify(data, null, 2)}</pre>
|
||||
`;
|
||||
}
|
||||
|
||||
htmlContent += `</div>`;
|
||||
outputElement.innerHTML = htmlContent;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
function handleTagElement(): void {
|
||||
// Aksi untuk menandai elemen
|
||||
// alert('Element tagged!');
|
||||
executeAction('tagElement');
|
||||
}
|
||||
|
||||
function handleSaveData(): void {
|
||||
// Aksi untuk menyimpan data
|
||||
executeAction('saveData');
|
||||
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const popupAction = document.querySelectorAll('[data-action]');
|
||||
popupAction.forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
const action = button.getAttribute('data-action');
|
||||
if (action && action in popupActionHandlers) {
|
||||
popupActionHandlers[action]();
|
||||
} else {
|
||||
console.error(`Unknown popup action: ${action}`);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const btnMenu = document.querySelector('.btn-menu');
|
||||
if (btnMenu) {
|
||||
btnMenu.addEventListener('click', () => {
|
||||
const menu = document.querySelector('.menu-container');
|
||||
if (menu) {
|
||||
menu.classList.toggle('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const toggleBtn = document.getElementById('toggle-sidebar-btn');
|
||||
if (toggleBtn) {
|
||||
toggleBtn.addEventListener('click', () => {
|
||||
// Ask the current tab to toggle the sidebar
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||
chrome.tabs.sendMessage(tabs[0].id!, { action: 'toggleSidebar' });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const formApiEndpoint = document.getElementById('form-api-endpoint') as HTMLFormElement;
|
||||
if (formApiEndpoint) {
|
||||
formApiEndpoint.addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
const input = formApiEndpoint.querySelector('input[name="api-endpoint"]') as HTMLInputElement;
|
||||
const apiURL = input.value.trim();
|
||||
if (apiURL.startsWith('http://') || apiURL.startsWith('https://')) {
|
||||
// Save the API endpoint to storage
|
||||
chrome.storage.local.set({ apiURL }, () => {
|
||||
// console.log('API endpoint saved:', apiURL);
|
||||
alert('API endpoint saved successfully!');
|
||||
});
|
||||
} else {
|
||||
alert('API endpoint cannot be empty / must start with http:// or https://');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
chrome.storage.local.get('apiURL', (result) => {
|
||||
const apiField = document.getElementById('field-api-endpoint') as HTMLInputElement;
|
||||
if (apiField) {
|
||||
apiField.value = result.apiURL || '';
|
||||
}
|
||||
console.log('Current API endpoint:', result.apiURL);
|
||||
});
|
||||
|
||||
});
|
||||
65
src/public/manifest.json
Normal file
65
src/public/manifest.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "New Browser Input",
|
||||
"version": "1.0",
|
||||
"description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, nisl nec ultricies lacinia, nunc nisl aliquet nunc, vitae aliquam nisl nunc vitae nisl. Sed euismod, nisl nec ultricies lacinia, nunc nisl aliquet nunc, vitae aliquam nisl nunc vitae nisl.",
|
||||
|
||||
"permissions": [
|
||||
"storage",
|
||||
"activeTab",
|
||||
"scripting",
|
||||
"webRequest",
|
||||
"webRequestBlocking"
|
||||
],
|
||||
|
||||
"host_permissions": [
|
||||
"https://x.com/*",
|
||||
"https://www.tiktok.com/*",
|
||||
"https://www.linkedin.com/*",
|
||||
"https://www.youtube.com/*",
|
||||
"https://www.facebook.com/*",
|
||||
"https://www.instagram.com/*"
|
||||
],
|
||||
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["popup/popup.html", "popup/popup.js", "popup/popup.css"],
|
||||
"matches": [
|
||||
"https://x.com/*",
|
||||
"https://www.tiktok.com/*",
|
||||
"https://www.linkedin.com/*",
|
||||
"https://www.youtube.com/*",
|
||||
"https://www.facebook.com/*",
|
||||
"https://www.instagram.com/*"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
"side_panel": {
|
||||
"default_path": "popup/popup.html"
|
||||
},
|
||||
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": [
|
||||
"https://x.com/*",
|
||||
"https://www.tiktok.com/*",
|
||||
"https://www.linkedin.com/*",
|
||||
"https://www.youtube.com/*",
|
||||
"https://www.facebook.com/*",
|
||||
"https://www.instagram.com/*"
|
||||
],
|
||||
"js": ["contentScript.js"],
|
||||
"css": ["mark.css"]
|
||||
}
|
||||
],
|
||||
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
|
||||
"action": {
|
||||
"default_title": "New Browser Input",
|
||||
"default_popup": "popup/popup.html"
|
||||
}
|
||||
}
|
||||
219
src/utils/downloadVideo.ts
Normal file
219
src/utils/downloadVideo.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
// Versi TypeScript dengan dukungan File System dan tipe data
|
||||
|
||||
import fs from 'fs';
|
||||
import https from 'https';
|
||||
import http from 'http';
|
||||
import { URL } from 'url';
|
||||
import path from 'path';
|
||||
|
||||
// Tipe untuk response hasil download menggunakan https
|
||||
interface HttpsDownloadResponse {
|
||||
ok: boolean;
|
||||
status: number;
|
||||
headers: http.IncomingHttpHeaders;
|
||||
arrayBuffer: () => Promise<ArrayBuffer>;
|
||||
}
|
||||
|
||||
// Fungsi untuk download video
|
||||
export async function downloadVideo(videoUrl: string, filename = 'video.mp4'): Promise<Buffer | null> {
|
||||
try {
|
||||
console.log('🔄 Downloading video from:', videoUrl);
|
||||
|
||||
let response: any;
|
||||
|
||||
if (typeof fetch !== 'undefined') {
|
||||
response = await fetch(videoUrl);
|
||||
} else {
|
||||
response = await downloadWithHttps(videoUrl);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get ?
|
||||
response.headers.get('content-type') :
|
||||
response.headers['content-type'];
|
||||
console.log('📁 Content Type:', contentType);
|
||||
|
||||
const contentLength = response.headers.get ?
|
||||
response.headers.get('content-length') :
|
||||
response.headers['content-length'];
|
||||
if (contentLength) {
|
||||
console.log('📊 File Size:', (parseInt(contentLength) / 1024 / 1024).toFixed(2) + ' MB');
|
||||
}
|
||||
|
||||
const videoBuffer = typeof fetch !== 'undefined' ?
|
||||
Buffer.from(await response.arrayBuffer()) :
|
||||
response as Buffer;
|
||||
|
||||
console.log('✅ Video downloaded successfully');
|
||||
await saveVideoToFile(videoBuffer, filename);
|
||||
return videoBuffer;
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('❌ Error downloading video:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function downloadWithHttps(videoUrl: string): Promise<HttpsDownloadResponse> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const urlObj = new URL(videoUrl);
|
||||
const protocol = urlObj.protocol === 'https:' ? https : http;
|
||||
|
||||
const request = protocol.get(videoUrl, (response) => {
|
||||
if (response.statusCode !== 200) {
|
||||
reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
|
||||
return;
|
||||
}
|
||||
|
||||
const chunks: Buffer[] = [];
|
||||
response.on('data', (chunk) => chunks.push(chunk));
|
||||
response.on('end', () => {
|
||||
const buffer = Buffer.concat(chunks);
|
||||
resolve({
|
||||
ok: true,
|
||||
status: response.statusCode || 200,
|
||||
headers: response.headers,
|
||||
arrayBuffer: () => Promise.resolve(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength))
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
request.on('error', reject);
|
||||
request.setTimeout(30000, () => {
|
||||
request.destroy();
|
||||
reject(new Error('Request timeout'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function saveVideoToFile(buffer: Buffer, filename: string): Promise<string> {
|
||||
try {
|
||||
const downloadsDir = path.join(process.cwd(), 'downloads');
|
||||
if (!fs.existsSync(downloadsDir)) {
|
||||
fs.mkdirSync(downloadsDir, { recursive: true });
|
||||
}
|
||||
|
||||
const filePath = path.join(downloadsDir, filename);
|
||||
await fs.promises.writeFile(filePath, buffer);
|
||||
|
||||
console.log('💾 File saved to:', filePath);
|
||||
console.log('📁 File size:', (buffer.length / 1024 / 1024).toFixed(2) + ' MB');
|
||||
|
||||
return filePath;
|
||||
} catch (error: any) {
|
||||
console.error('❌ Error saving file:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getVideoInfo(videoUrl: string): Promise<any> {
|
||||
const urlObj = new URL(videoUrl);
|
||||
const protocol = urlObj.protocol === 'https:' ? https : http;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = protocol.request(videoUrl, { method: 'HEAD' }, (response) => {
|
||||
resolve({
|
||||
url: videoUrl,
|
||||
status: response.statusCode,
|
||||
contentType: response.headers['content-type'],
|
||||
contentLength: response.headers['content-length'],
|
||||
lastModified: response.headers['last-modified'],
|
||||
size: response.headers['content-length'] ?
|
||||
(parseInt(response.headers['content-length']) / 1024 / 1024).toFixed(2) + ' MB' : 'Unknown'
|
||||
});
|
||||
});
|
||||
|
||||
request.on('error', reject);
|
||||
request.setTimeout(10000, () => {
|
||||
request.destroy();
|
||||
reject(new Error('Request timeout'));
|
||||
});
|
||||
|
||||
request.end();
|
||||
});
|
||||
}
|
||||
|
||||
export async function downloadVideoWithProgress(videoUrl: string, filename = 'video.mp4'): Promise<{ buffer: Buffer; filePath: string; size: number; } | null> {
|
||||
try {
|
||||
console.log('🔄 Starting download with progress tracking...');
|
||||
|
||||
const url = new URL(videoUrl);
|
||||
const protocol = url.protocol === 'https:' ? https : http;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = protocol.get(videoUrl, (response) => {
|
||||
if (response.statusCode !== 200) {
|
||||
reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`));
|
||||
return;
|
||||
}
|
||||
|
||||
const total = parseInt(response.headers['content-length'] || '0', 10);
|
||||
let loaded = 0;
|
||||
const chunks: Buffer[] = [];
|
||||
|
||||
console.log('📊 Total size:', (total / 1024 / 1024).toFixed(2) + ' MB');
|
||||
|
||||
response.on('data', (chunk) => {
|
||||
chunks.push(chunk);
|
||||
loaded += chunk.length;
|
||||
if (total) {
|
||||
const progress = Math.round((loaded / total) * 100);
|
||||
const loadedMB = (loaded / 1024 / 1024).toFixed(2);
|
||||
const totalMB = (total / 1024 / 1024).toFixed(2);
|
||||
process.stdout.write(`\r📥 Progress: ${progress}% (${loadedMB}/${totalMB} MB)`);
|
||||
}
|
||||
});
|
||||
|
||||
response.on('end', async () => {
|
||||
console.log('\n✅ Download completed!');
|
||||
const buffer = Buffer.concat(chunks);
|
||||
try {
|
||||
const filePath = await saveVideoToFile(buffer, filename);
|
||||
resolve({ buffer, filePath, size: buffer.length });
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
request.on('error', reject);
|
||||
request.setTimeout(60000, () => {
|
||||
request.destroy();
|
||||
reject(new Error('Request timeout'));
|
||||
});
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Download failed:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function testDownload(): Promise<void> {
|
||||
const videoUrl = 'https://your.video.url.mp4';
|
||||
console.log('=== Testing Video Download in Node.js ===');
|
||||
|
||||
try {
|
||||
console.log('\n1. Getting video info...');
|
||||
const info = await getVideoInfo(videoUrl);
|
||||
console.log('Video Info:', info);
|
||||
|
||||
console.log('\n2. Downloading video with progress...');
|
||||
const result = await downloadVideoWithProgress(videoUrl, 'instagram_video.mp4');
|
||||
|
||||
if (result) {
|
||||
console.log('✅ Download completed!');
|
||||
console.log('📁 File saved to:', result.filePath);
|
||||
console.log('📊 File size:', (result.size / 1024 / 1024).toFixed(2) + ' MB');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('❌ Test failed:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
testDownload();
|
||||
}
|
||||
4
src/utils/turndown-plugin-gfm.d.ts
vendored
Normal file
4
src/utils/turndown-plugin-gfm.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module 'turndown-plugin-gfm' {
|
||||
import { Plugin } from 'turndown';
|
||||
export const gfm: Plugin;
|
||||
}
|
||||
47
src/utils/turndown.ts
Normal file
47
src/utils/turndown.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import TurndownService from 'turndown';
|
||||
import { gfm } from 'turndown-plugin-gfm';
|
||||
|
||||
export const turndownService = new TurndownService();
|
||||
turndownService.use(gfm);
|
||||
|
||||
// bug table
|
||||
const tableRule = turndownService.rules.array[2];
|
||||
if (!tableRule.filter.toString().includes('TABLE'))
|
||||
throw new Error('Incorrect rule selected. Expected to find table rule');
|
||||
tableRule.filter = ['table'];
|
||||
|
||||
// Filter element button youtube
|
||||
turndownService.addRule('filterButtonContainer', {
|
||||
filter: function (node) {
|
||||
return node.id === 'button-container';
|
||||
},
|
||||
replacement: function () {
|
||||
return ''; // Hilangkan element ini sepenuhnya
|
||||
}
|
||||
});
|
||||
|
||||
// Filter element YouTube
|
||||
turndownService.addRule('youtubeElements', {
|
||||
filter: function (node) {
|
||||
// Filter semua element yang dimulai dengan 'yt-' atau 'ytd-'
|
||||
if (node.nodeName && (node.nodeName.toLowerCase().startsWith('yt-') ||
|
||||
node.nodeName.toLowerCase().startsWith('ytd-'))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
replacement: function (content) {
|
||||
// Ambil teks dari dalam element YouTube
|
||||
return content;
|
||||
}
|
||||
});
|
||||
|
||||
// Filter element yang tersembunyi
|
||||
turndownService.addRule('hiddenElements', {
|
||||
filter: function (node) {
|
||||
return node.hasAttribute && node.hasAttribute('hidden');
|
||||
},
|
||||
replacement: function () {
|
||||
return '';
|
||||
}
|
||||
});
|
||||
60
tmp/timeConverter.js
Normal file
60
tmp/timeConverter.js
Normal file
@@ -0,0 +1,60 @@
|
||||
const timeConverter = (hoursAgo) => {
|
||||
const date = new Date();
|
||||
date.setTime(date.getTime() - (parseInt(hoursAgo) * 60 * 60 * 1000));
|
||||
|
||||
return {
|
||||
indonesian: date.toLocaleString('id-ID', {
|
||||
timeZone: 'Asia/Jakarta'
|
||||
}),
|
||||
iso: date.toISOString()
|
||||
};
|
||||
};
|
||||
|
||||
const parseRelativeTime = (relativeTimeString) => {
|
||||
const match = relativeTimeString.match(/(\d+)(hr|h|hours?|min|m|minutes?|d|days?)\s*ago/i);
|
||||
|
||||
if (!match) {
|
||||
throw new Error('Invalid relative time format');
|
||||
}
|
||||
|
||||
const value = parseInt(match[1]);
|
||||
const unit = match[2].toLowerCase();
|
||||
|
||||
const date = new Date();
|
||||
|
||||
switch (unit) {
|
||||
case 'hr':
|
||||
case 'h':
|
||||
case 'hour':
|
||||
case 'hours':
|
||||
date.setTime(date.getTime() - (value * 60 * 60 * 1000));
|
||||
break;
|
||||
case 'min':
|
||||
case 'm':
|
||||
case 'minute':
|
||||
case 'minutes':
|
||||
date.setTime(date.getTime() - (value * 60 * 1000));
|
||||
break;
|
||||
case 'd':
|
||||
case 'day':
|
||||
case 'days':
|
||||
date.setTime(date.getTime() - (value * 24 * 60 * 60 * 1000));
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unsupported time unit');
|
||||
}
|
||||
|
||||
return {
|
||||
indonesian: date.toLocaleString('id-ID', {
|
||||
timeZone: 'Asia/Jakarta'
|
||||
}),
|
||||
iso: date.toISOString(),
|
||||
timestamp: date.getTime()
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
console.log('4 hours ago:', parseRelativeTime('4hr ago'));
|
||||
console.log('30 minutes ago:', parseRelativeTime('30min ago'));
|
||||
console.log('2 days ago:', parseRelativeTime('2d ago'));
|
||||
console.log('Original timeConverter:', timeConverter('4'));
|
||||
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"ignoreDeprecations": "6.0"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user