diff --git a/.gitignore b/.gitignore index 3fe46ea01..ad225b41d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ node_modules .secrets .actrc .DS_Store -.gh-pages -.api +/.gh-pages/ +/.api/ .env -/temp \ No newline at end of file +/temp/ \ No newline at end of file diff --git a/.readme/template.md b/.readme/template.md index 5dd4d1cf8..84a8f7013 100644 --- a/.readme/template.md +++ b/.readme/template.md @@ -93,12 +93,12 @@ Same thing, but split up into separate files: ### Grouped by region +Playlists in which channels are grouped by the region for which they are broadcasted. +
Expand
-Playlists in which channels are grouped by the region for which they are broadcasted. - ``` https://iptv-org.github.io/iptv/index.region.m3u ``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6ed2f6f4c..1ddaf545d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -176,6 +176,7 @@ To run scripts use the `npm run ` command. - `playlist:validate`: сhecks ids and links in internal playlists for errors. - `playlist:lint`: сhecks internal playlists for syntax errors. - `playlist:test`: tests links in internal playlists. +- `playlist:edit`: utility for quick streams mapping. - `playlist:deploy`: allows to manually publish all generated via `playlist:generate` playlists. To run the script you must provide your [personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) with write access to the repository. - `readme:update`: updates the list of playlists in [README.md](README.md). - `report:create`: creates a report on current issues. diff --git a/package-lock.json b/package-lock.json index a9d29de73..e09fd3949 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,12 +10,15 @@ "dependencies": { "@eslint/eslintrc": "^3.3.0", "@eslint/js": "^9.21.0", - "@freearhey/core": "^0.7.0", + "@freearhey/core": "^0.8.2", + "@freearhey/search-js": "^0.1.2", + "@inquirer/prompts": "^7.4.1", "@octokit/core": "^6.1.4", "@octokit/plugin-paginate-rest": "^11.4.3", "@octokit/plugin-rest-endpoint-methods": "^7.1.3", "@octokit/types": "^11.1.0", "@types/cli-progress": "^3.11.3", + "@types/fs-extra": "^11.0.4", "@types/jest": "^29.5.14", "@types/lodash": "^4.14.198", "@types/numeral": "^2.0.3", @@ -29,6 +32,7 @@ "commander": "^8.3.0", "console-table-printer": "^2.12.1", "eslint": "^9.17.0", + "glob": "^11.0.2", "globals": "^16.0.0", "iptv-checker": "^0.29.1", "iptv-playlist-parser": "^0.13.0", @@ -36,8 +40,8 @@ "lodash": "^4.17.21", "m3u-linter": "^0.4.2", "markdown-include": "^0.4.3", + "node-cleanup": "^2.1.2", "numeral": "^2.0.6", - "transliteration": "^2.3.5", "ts-jest": "^29.2.5", "tsx": "^4.6.2", "valid-url": "^1.0.9" @@ -1063,23 +1067,20 @@ } }, "node_modules/@freearhey/core": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@freearhey/core/-/core-0.7.0.tgz", - "integrity": "sha512-HXkKPYGY7ife7JAc1q/Qxzy0WUdSnyt3rHThCShZHgnH3rz0tpkjHFW7LNegB3he0IKn/Zc95/YSOQ97Fq8ctA==", + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@freearhey/core/-/core-0.8.2.tgz", + "integrity": "sha512-jlb1XUbhUf3lqD3B9Wmx3c8qYG4+s1I0cr2FFQfiMpJh4nMvfUNdJr2OhH31S/dbNP12ycT6RPVoZ2j2G3+mXA==", "dependencies": { - "@types/fs-extra": "^11.0.2", - "@types/lodash": "^4.14.198", - "@types/luxon": "^3.3.2", - "fs-extra": "^11.1.1", - "glob": "^10.3.4", + "consola": "^3.4.2", + "dayjs": "^1.11.13", + "fs-extra": "^11.3.0", + "glob": "^11.0.1", "lodash": "^4.17.21", - "luxon": "^3.4.3", - "natural-orderby": "^3.0.2", - "node-gzip": "^1.1.2", + "natural-orderby": "^5.0.0", "normalize-url": "^6.1.0", "object-treeify": "^2.1.1", - "run-script-os": "^1.1.6", - "signale": "^1.4.0" + "pako": "^2.1.0", + "timer-node": "^5.0.9" } }, "node_modules/@freearhey/core/node_modules/normalize-url": { @@ -1093,6 +1094,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@freearhey/search-js": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@freearhey/search-js/-/search-js-0.1.2.tgz", + "integrity": "sha512-F2o+xpGCXOK4OsZfKEHfXNNkAZmny2eBnPOp+P0iyV20ja7gJGfTFaEc6okcuEo6OB6P7LnSxTvISkoArFtlfg==", + "dependencies": { + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1149,13 +1161,36 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@inquirer/confirm": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.7.tgz", - "integrity": "sha512-Xrfbrw9eSiHb+GsesO8TQIeHSMTP0xyvTCeeYevgZ4sKW+iz9w/47bgfG9b0niQm+xaLY2EWPBINUPldLwvYiw==", + "node_modules/@inquirer/checkbox": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.5.tgz", + "integrity": "sha512-swPczVU+at65xa5uPfNP9u3qx/alNwiaykiI/ExpsmMSQW55trmZcwhYWzw/7fj+n6Q8z1eENvR7vFfq9oPSAQ==", "dependencies": { - "@inquirer/core": "^10.1.8", - "@inquirer/type": "^3.0.5" + "@inquirer/core": "^10.1.10", + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.6", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.9.tgz", + "integrity": "sha512-NgQCnHqFTjF7Ys2fsqK2WtnA8X1kHyInyG+nMIuHowVTIgIuS10T4AznI/PvbqSpJqjCUqNBlKGh1v3bwLFL4w==", + "dependencies": { + "@inquirer/core": "^10.1.10", + "@inquirer/type": "^3.0.6" }, "engines": { "node": ">=18" @@ -1170,12 +1205,12 @@ } }, "node_modules/@inquirer/core": { - "version": "10.1.8", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.8.tgz", - "integrity": "sha512-HpAqR8y715zPpM9e/9Q+N88bnGwqqL8ePgZ0SMv/s3673JLMv3bIkoivGmjPqXlEgisUksSXibweQccUwEx4qQ==", + "version": "10.1.10", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.10.tgz", + "integrity": "sha512-roDaKeY1PYY0aCqhRmXihrHjoSW2A00pV3Ke5fTpMCkzcGF64R8e0lw3dK+eLEHwS4vB5RnW1wuQmvzoRul8Mw==", "dependencies": { "@inquirer/figures": "^1.0.11", - "@inquirer/type": "^3.0.5", + "@inquirer/type": "^3.0.6", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", @@ -1219,6 +1254,48 @@ "node": ">=8" } }, + "node_modules/@inquirer/editor": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.10.tgz", + "integrity": "sha512-5GVWJ+qeI6BzR6TIInLP9SXhWCEcvgFQYmcRG6d6RIlhFjM5TyG18paTGBgRYyEouvCmzeco47x9zX9tQEofkw==", + "dependencies": { + "@inquirer/core": "^10.1.10", + "@inquirer/type": "^3.0.6", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.12.tgz", + "integrity": "sha512-jV8QoZE1fC0vPe6TnsOfig+qwu7Iza1pkXoUJ3SroRagrt2hxiL+RbM432YAihNR7m7XnU0HWl/WQ35RIGmXHw==", + "dependencies": { + "@inquirer/core": "^10.1.10", + "@inquirer/type": "^3.0.6", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@inquirer/figures": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.11.tgz", @@ -1227,10 +1304,165 @@ "node": ">=18" } }, + "node_modules/@inquirer/input": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.9.tgz", + "integrity": "sha512-mshNG24Ij5KqsQtOZMgj5TwEjIf+F2HOESk6bjMwGWgcH5UBe8UoljwzNFHqdMbGYbgAf6v2wU/X9CAdKJzgOA==", + "dependencies": { + "@inquirer/core": "^10.1.10", + "@inquirer/type": "^3.0.6" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.12.tgz", + "integrity": "sha512-7HRFHxbPCA4e4jMxTQglHJwP+v/kpFsCf2szzfBHy98Wlc3L08HL76UDiA87TOdX5fwj2HMOLWqRWv9Pnn+Z5Q==", + "dependencies": { + "@inquirer/core": "^10.1.10", + "@inquirer/type": "^3.0.6" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.12.tgz", + "integrity": "sha512-FlOB0zvuELPEbnBYiPaOdJIaDzb2PmJ7ghi/SVwIHDDSQ2K4opGBkF+5kXOg6ucrtSUQdLhVVY5tycH0j0l+0g==", + "dependencies": { + "@inquirer/core": "^10.1.10", + "@inquirer/type": "^3.0.6", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.4.1.tgz", + "integrity": "sha512-UlmM5FVOZF0gpoe1PT/jN4vk8JmpIWBlMvTL8M+hlvPmzN89K6z03+IFmyeu/oFCenwdwHDr2gky7nIGSEVvlA==", + "dependencies": { + "@inquirer/checkbox": "^4.1.5", + "@inquirer/confirm": "^5.1.9", + "@inquirer/editor": "^4.2.10", + "@inquirer/expand": "^4.0.12", + "@inquirer/input": "^4.1.9", + "@inquirer/number": "^3.0.12", + "@inquirer/password": "^4.0.12", + "@inquirer/rawlist": "^4.0.12", + "@inquirer/search": "^3.0.12", + "@inquirer/select": "^4.1.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.0.12.tgz", + "integrity": "sha512-wNPJZy8Oc7RyGISPxp9/MpTOqX8lr0r+lCCWm7hQra+MDtYRgINv1hxw7R+vKP71Bu/3LszabxOodfV/uTfsaA==", + "dependencies": { + "@inquirer/core": "^10.1.10", + "@inquirer/type": "^3.0.6", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.12.tgz", + "integrity": "sha512-H/kDJA3kNlnNIjB8YsaXoQI0Qccgf0Na14K1h8ExWhNmUg2E941dyFPrZeugihEa9AZNW5NdsD/NcvUME83OPQ==", + "dependencies": { + "@inquirer/core": "^10.1.10", + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.6", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.1.1.tgz", + "integrity": "sha512-IUXzzTKVdiVNMA+2yUvPxWsSgOG4kfX93jOM4Zb5FgujeInotv5SPIJVeXQ+fO4xu7tW8VowFhdG5JRmmCyQ1Q==", + "dependencies": { + "@inquirer/core": "^10.1.10", + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.6", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@inquirer/type": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.5.tgz", - "integrity": "sha512-ZJpeIYYueOz/i/ONzrfof8g89kNdO2hjGuvULROo3O8rlB2CRtSseE5KeirnyE4t/thAn/EwvS/vuQeJCn+NZg==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.6.tgz", + "integrity": "sha512-/mKVCtVpyBu3IDarv0G+59KC4stsD5mDsGpYh+GKs1NZT88Jh52+cuoA1AtLk2Q0r/quNl+1cSUyLRHBFeD0XA==", "engines": { "node": ">=18" }, @@ -1260,9 +1492,9 @@ } }, "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "engines": { "node": ">=12" }, @@ -2054,15 +2286,6 @@ "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==" }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -2146,9 +2369,9 @@ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" }, "node_modules/@types/fs-extra": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.2.tgz", - "integrity": "sha512-c0hrgAOVYr21EX8J0jBMXGLMgJqVf/v6yxi0dLaJboW9aQPh16Id+z6w2Tx1hm+piJOLv8xPfVKZCLfjPw/IMQ==", + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", "dependencies": { "@types/jsonfile": "*", "@types/node": "*" @@ -2199,9 +2422,9 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, "node_modules/@types/jsonfile": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.1.tgz", - "integrity": "sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", + "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", "dependencies": { "@types/node": "*" } @@ -2211,11 +2434,6 @@ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.198.tgz", "integrity": "sha512-trNJ/vtMZYMLhfN45uLq4ShQSw0/S7xCTLLVM+WM1rmFpba/VS42jVUgaO3w/NOLiWR/09lnYk0yMaA/atdIsg==" }, - "node_modules/@types/luxon": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.2.tgz", - "integrity": "sha512-l5cpE57br4BIjK+9BSkFBOsWtwv6J9bJpC7gdXIzZyI0vuKvNTk0wZZrkQxMGsUAuGW9+WMNWF2IJMD7br2yeQ==" - }, "node_modules/@types/node": { "version": "22.13.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", @@ -2862,6 +3080,11 @@ "node": ">=10" } }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -2983,6 +3206,14 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/console-table-printer": { "version": "2.12.1", "resolved": "https://registry.npmjs.org/console-table-printer/-/console-table-printer-2.12.1.tgz", @@ -3047,6 +3278,11 @@ "node": ">=12.20" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -3162,6 +3398,7 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "peer": true, "dependencies": { "is-arrayish": "^0.2.1" } @@ -3169,7 +3406,8 @@ "node_modules/error-ex/node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "peer": true }, "node_modules/esbuild": { "version": "0.25.1", @@ -3447,6 +3685,19 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/fast-content-type-parse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz", @@ -3539,25 +3790,6 @@ "bser": "2.1.1" } }, - "node_modules/figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/figures/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -3698,9 +3930,9 @@ } }, "node_modules/fs-extra": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", - "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -3711,9 +3943,9 @@ } }, "node_modules/fs-extra/node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "engines": { "node": ">= 10.0.0" } @@ -3807,21 +4039,22 @@ } }, "node_modules/glob": { - "version": "10.3.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.4.tgz", - "integrity": "sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.2.tgz", + "integrity": "sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==", "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.0.3", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" }, "bin": { - "glob": "dist/cjs/src/bin.js" + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -3866,14 +4099,14 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", "dependencies": { "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -3948,6 +4181,17 @@ "node": ">=10.17.0" } }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4267,20 +4511,17 @@ } }, "node_modules/jackspeak": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.3.tgz", - "integrity": "sha512-R2bUw+kVZFS/h1AZqBKrSgDmdmjApzgY0AlCPumopFiAlbUxE2gf+SCuBzQ0cP5hHmUmFYF5yw55T97Th5Kstg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz", + "integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==", "dependencies": { "@isaacs/cliui": "^8.0.2" }, "engines": { - "node": ">=14" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" } }, "node_modules/jake": { @@ -4935,11 +5176,6 @@ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" }, - "node_modules/json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" - }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -4979,9 +5215,9 @@ } }, "node_modules/jsonfile/node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "engines": { "node": ">= 10.0.0" } @@ -5030,40 +5266,6 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "peer": true }, - "node_modules/load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", - "dependencies": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/load-json-file/node_modules/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dependencies": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/load-json-file/node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "engines": { - "node": ">=4" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5102,14 +5304,6 @@ "yallist": "^3.0.2" } }, - "node_modules/luxon": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.3.tgz", - "integrity": "sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg==", - "engines": { - "node": ">=12" - } - }, "node_modules/m3u-linter": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/m3u-linter/-/m3u-linter-0.4.2.tgz", @@ -5127,14 +5321,6 @@ "node": ">=10.0.0" } }, - "node_modules/m3u-linter/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/m3u-linter/node_modules/commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", @@ -5143,79 +5329,6 @@ "node": ">= 10" } }, - "node_modules/m3u-linter/node_modules/glob": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", - "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^4.0.1", - "minimatch": "^10.0.0", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/m3u-linter/node_modules/jackspeak": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", - "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/m3u-linter/node_modules/lru-cache": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", - "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/m3u-linter/node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/m3u-linter/node_modules/path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -5402,17 +5515,17 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" }, "node_modules/natural-orderby": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/natural-orderby/-/natural-orderby-3.0.2.tgz", - "integrity": "sha512-x7ZdOwBxZCEm9MM7+eQCjkrNLrW3rkBKNHVr78zbtqnMGVNlnDi6C/eUEYgxHNrcbu0ymvjzcwIL/6H1iHri9g==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/natural-orderby/-/natural-orderby-5.0.0.tgz", + "integrity": "sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg==", "engines": { "node": ">=18" } }, - "node_modules/node-gzip": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/node-gzip/-/node-gzip-1.1.2.tgz", - "integrity": "sha512-ZB6zWpfZHGtxZnPMrJSKHVPrRjURoUzaDbLFj3VO70mpLTW5np96vXyHwft4Id0o+PYIzgDkBUjIzaNHhQ8srw==" + "node_modules/node-cleanup": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/node-cleanup/-/node-cleanup-2.1.2.tgz", + "integrity": "sha512-qN8v/s2PAJwGUtr1/hYTpNKlD6Y9rc4p8KSmJXyGdYGZsDGKXrGThikLFP9OCHFeLeEpQzPwiAtdIvBLqm//Hw==" }, "node_modules/node-int64": { "version": "0.4.0", @@ -5514,6 +5627,14 @@ "node": ">= 0.8.0" } }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/outvariant": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", @@ -5561,6 +5682,11 @@ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5622,26 +5748,26 @@ "peer": true }, "node_modules/path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", - "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", + "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", "engines": { - "node": "14 || >=16.14" + "node": "20 || >=22" } }, "node_modules/path-to-regexp": { @@ -5665,14 +5791,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "engines": { - "node": ">=4" - } - }, "node_modules/pirates": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", @@ -5682,79 +5800,6 @@ "node": ">= 6" } }, - "node_modules/pkg-conf": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-2.1.0.tgz", - "integrity": "sha1-ISZRTKbyq/69FoWW3xi6V4Z/AFg=", - "dependencies": { - "find-up": "^2.0.0", - "load-json-file": "^4.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pkg-conf/node_modules/find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dependencies": { - "locate-path": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pkg-conf/node_modules/locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dependencies": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pkg-conf/node_modules/p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dependencies": { - "p-try": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pkg-conf/node_modules/p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dependencies": { - "p-limit": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pkg-conf/node_modules/p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "engines": { - "node": ">=4" - } - }, - "node_modules/pkg-conf/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "engines": { - "node": ">=4" - } - }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -6060,14 +6105,10 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/run-script-os": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/run-script-os/-/run-script-os-1.1.6.tgz", - "integrity": "sha512-ql6P2LzhBTTDfzKts+Qo4H94VUKpxKDFz6QxxwaUZN0mwvi7L3lpOI7BqPCq7lgDh3XLl0dpeXwfcVIitlrYrw==", - "bin": { - "run-os": "index.js", - "run-script-os": "index.js" - } + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/semver": { "version": "7.6.3", @@ -6105,83 +6146,6 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "peer": true }, - "node_modules/signale": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/signale/-/signale-1.4.0.tgz", - "integrity": "sha512-iuh+gPf28RkltuJC7W5MRi6XAjTDCAPC/prJUpQoG4vIP3MJZ+GTydVnodXA7pwvTKb2cA0m9OFZW/cdWy/I/w==", - "dependencies": { - "chalk": "^2.3.2", - "figures": "^2.0.0", - "pkg-conf": "^2.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/signale/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/signale/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/signale/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/signale/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "node_modules/signale/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/signale/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "engines": { - "node": ">=4" - } - }, - "node_modules/signale/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/simple-wcswidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.0.1.tgz", @@ -6443,6 +6407,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/timer-node": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/timer-node/-/timer-node-5.0.9.tgz", + "integrity": "sha512-zXxCE/5/YDi0hY9pygqgRqjRbrFRzigYxOudG0I3syaqAAmX9/w9sxex1bNFCN6c1S66RwPtEIJv65dN+1psew==" + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -6474,21 +6454,6 @@ "node": ">=6" } }, - "node_modules/transliteration": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/transliteration/-/transliteration-2.3.5.tgz", - "integrity": "sha512-HAGI4Lq4Q9dZ3Utu2phaWgtm3vB6PkLUFqWAScg/UW+1eZ/Tg6Exo4oC0/3VUol/w4BlefLhUUSVBr/9/ZGQOw==", - "dependencies": { - "yargs": "^17.5.1" - }, - "bin": { - "slugify": "dist/bin/slugify", - "transliterate": "dist/bin/transliterate" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/ts-api-utils": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", @@ -7465,23 +7430,20 @@ } }, "@freearhey/core": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@freearhey/core/-/core-0.7.0.tgz", - "integrity": "sha512-HXkKPYGY7ife7JAc1q/Qxzy0WUdSnyt3rHThCShZHgnH3rz0tpkjHFW7LNegB3he0IKn/Zc95/YSOQ97Fq8ctA==", + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@freearhey/core/-/core-0.8.2.tgz", + "integrity": "sha512-jlb1XUbhUf3lqD3B9Wmx3c8qYG4+s1I0cr2FFQfiMpJh4nMvfUNdJr2OhH31S/dbNP12ycT6RPVoZ2j2G3+mXA==", "requires": { - "@types/fs-extra": "^11.0.2", - "@types/lodash": "^4.14.198", - "@types/luxon": "^3.3.2", - "fs-extra": "^11.1.1", - "glob": "^10.3.4", + "consola": "^3.4.2", + "dayjs": "^1.11.13", + "fs-extra": "^11.3.0", + "glob": "^11.0.1", "lodash": "^4.17.21", - "luxon": "^3.4.3", - "natural-orderby": "^3.0.2", - "node-gzip": "^1.1.2", + "natural-orderby": "^5.0.0", "normalize-url": "^6.1.0", "object-treeify": "^2.1.1", - "run-script-os": "^1.1.6", - "signale": "^1.4.0" + "pako": "^2.1.0", + "timer-node": "^5.0.9" }, "dependencies": { "normalize-url": { @@ -7491,6 +7453,14 @@ } } }, + "@freearhey/search-js": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@freearhey/search-js/-/search-js-0.1.2.tgz", + "integrity": "sha512-F2o+xpGCXOK4OsZfKEHfXNNkAZmny2eBnPOp+P0iyV20ja7gJGfTFaEc6okcuEo6OB6P7LnSxTvISkoArFtlfg==", + "requires": { + "lodash": "^4.17.21" + } + }, "@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -7522,22 +7492,34 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==" }, - "@inquirer/confirm": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.7.tgz", - "integrity": "sha512-Xrfbrw9eSiHb+GsesO8TQIeHSMTP0xyvTCeeYevgZ4sKW+iz9w/47bgfG9b0niQm+xaLY2EWPBINUPldLwvYiw==", + "@inquirer/checkbox": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.5.tgz", + "integrity": "sha512-swPczVU+at65xa5uPfNP9u3qx/alNwiaykiI/ExpsmMSQW55trmZcwhYWzw/7fj+n6Q8z1eENvR7vFfq9oPSAQ==", "requires": { - "@inquirer/core": "^10.1.8", - "@inquirer/type": "^3.0.5" + "@inquirer/core": "^10.1.10", + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.6", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + } + }, + "@inquirer/confirm": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.9.tgz", + "integrity": "sha512-NgQCnHqFTjF7Ys2fsqK2WtnA8X1kHyInyG+nMIuHowVTIgIuS10T4AznI/PvbqSpJqjCUqNBlKGh1v3bwLFL4w==", + "requires": { + "@inquirer/core": "^10.1.10", + "@inquirer/type": "^3.0.6" } }, "@inquirer/core": { - "version": "10.1.8", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.8.tgz", - "integrity": "sha512-HpAqR8y715zPpM9e/9Q+N88bnGwqqL8ePgZ0SMv/s3673JLMv3bIkoivGmjPqXlEgisUksSXibweQccUwEx4qQ==", + "version": "10.1.10", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.10.tgz", + "integrity": "sha512-roDaKeY1PYY0aCqhRmXihrHjoSW2A00pV3Ke5fTpMCkzcGF64R8e0lw3dK+eLEHwS4vB5RnW1wuQmvzoRul8Mw==", "requires": { "@inquirer/figures": "^1.0.11", - "@inquirer/type": "^3.0.5", + "@inquirer/type": "^3.0.6", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", @@ -7563,15 +7545,113 @@ } } }, + "@inquirer/editor": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.10.tgz", + "integrity": "sha512-5GVWJ+qeI6BzR6TIInLP9SXhWCEcvgFQYmcRG6d6RIlhFjM5TyG18paTGBgRYyEouvCmzeco47x9zX9tQEofkw==", + "requires": { + "@inquirer/core": "^10.1.10", + "@inquirer/type": "^3.0.6", + "external-editor": "^3.1.0" + } + }, + "@inquirer/expand": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.12.tgz", + "integrity": "sha512-jV8QoZE1fC0vPe6TnsOfig+qwu7Iza1pkXoUJ3SroRagrt2hxiL+RbM432YAihNR7m7XnU0HWl/WQ35RIGmXHw==", + "requires": { + "@inquirer/core": "^10.1.10", + "@inquirer/type": "^3.0.6", + "yoctocolors-cjs": "^2.1.2" + } + }, "@inquirer/figures": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.11.tgz", "integrity": "sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==" }, + "@inquirer/input": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.9.tgz", + "integrity": "sha512-mshNG24Ij5KqsQtOZMgj5TwEjIf+F2HOESk6bjMwGWgcH5UBe8UoljwzNFHqdMbGYbgAf6v2wU/X9CAdKJzgOA==", + "requires": { + "@inquirer/core": "^10.1.10", + "@inquirer/type": "^3.0.6" + } + }, + "@inquirer/number": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.12.tgz", + "integrity": "sha512-7HRFHxbPCA4e4jMxTQglHJwP+v/kpFsCf2szzfBHy98Wlc3L08HL76UDiA87TOdX5fwj2HMOLWqRWv9Pnn+Z5Q==", + "requires": { + "@inquirer/core": "^10.1.10", + "@inquirer/type": "^3.0.6" + } + }, + "@inquirer/password": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.12.tgz", + "integrity": "sha512-FlOB0zvuELPEbnBYiPaOdJIaDzb2PmJ7ghi/SVwIHDDSQ2K4opGBkF+5kXOg6ucrtSUQdLhVVY5tycH0j0l+0g==", + "requires": { + "@inquirer/core": "^10.1.10", + "@inquirer/type": "^3.0.6", + "ansi-escapes": "^4.3.2" + } + }, + "@inquirer/prompts": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.4.1.tgz", + "integrity": "sha512-UlmM5FVOZF0gpoe1PT/jN4vk8JmpIWBlMvTL8M+hlvPmzN89K6z03+IFmyeu/oFCenwdwHDr2gky7nIGSEVvlA==", + "requires": { + "@inquirer/checkbox": "^4.1.5", + "@inquirer/confirm": "^5.1.9", + "@inquirer/editor": "^4.2.10", + "@inquirer/expand": "^4.0.12", + "@inquirer/input": "^4.1.9", + "@inquirer/number": "^3.0.12", + "@inquirer/password": "^4.0.12", + "@inquirer/rawlist": "^4.0.12", + "@inquirer/search": "^3.0.12", + "@inquirer/select": "^4.1.1" + } + }, + "@inquirer/rawlist": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.0.12.tgz", + "integrity": "sha512-wNPJZy8Oc7RyGISPxp9/MpTOqX8lr0r+lCCWm7hQra+MDtYRgINv1hxw7R+vKP71Bu/3LszabxOodfV/uTfsaA==", + "requires": { + "@inquirer/core": "^10.1.10", + "@inquirer/type": "^3.0.6", + "yoctocolors-cjs": "^2.1.2" + } + }, + "@inquirer/search": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.12.tgz", + "integrity": "sha512-H/kDJA3kNlnNIjB8YsaXoQI0Qccgf0Na14K1h8ExWhNmUg2E941dyFPrZeugihEa9AZNW5NdsD/NcvUME83OPQ==", + "requires": { + "@inquirer/core": "^10.1.10", + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.6", + "yoctocolors-cjs": "^2.1.2" + } + }, + "@inquirer/select": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.1.1.tgz", + "integrity": "sha512-IUXzzTKVdiVNMA+2yUvPxWsSgOG4kfX93jOM4Zb5FgujeInotv5SPIJVeXQ+fO4xu7tW8VowFhdG5JRmmCyQ1Q==", + "requires": { + "@inquirer/core": "^10.1.10", + "@inquirer/figures": "^1.0.11", + "@inquirer/type": "^3.0.6", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + } + }, "@inquirer/type": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.5.tgz", - "integrity": "sha512-ZJpeIYYueOz/i/ONzrfof8g89kNdO2hjGuvULROo3O8rlB2CRtSseE5KeirnyE4t/thAn/EwvS/vuQeJCn+NZg==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.6.tgz", + "integrity": "sha512-/mKVCtVpyBu3IDarv0G+59KC4stsD5mDsGpYh+GKs1NZT88Jh52+cuoA1AtLk2Q0r/quNl+1cSUyLRHBFeD0XA==", "requires": {} }, "@isaacs/cliui": { @@ -7588,9 +7668,9 @@ }, "dependencies": { "ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==" + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==" }, "ansi-styles": { "version": "6.2.1", @@ -8229,12 +8309,6 @@ "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==" }, - "@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "optional": true - }, "@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -8318,9 +8392,9 @@ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" }, "@types/fs-extra": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.2.tgz", - "integrity": "sha512-c0hrgAOVYr21EX8J0jBMXGLMgJqVf/v6yxi0dLaJboW9aQPh16Id+z6w2Tx1hm+piJOLv8xPfVKZCLfjPw/IMQ==", + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", "requires": { "@types/jsonfile": "*", "@types/node": "*" @@ -8371,9 +8445,9 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, "@types/jsonfile": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.1.tgz", - "integrity": "sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", + "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", "requires": { "@types/node": "*" } @@ -8383,11 +8457,6 @@ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.198.tgz", "integrity": "sha512-trNJ/vtMZYMLhfN45uLq4ShQSw0/S7xCTLLVM+WM1rmFpba/VS42jVUgaO3w/NOLiWR/09lnYk0yMaA/atdIsg==" }, - "@types/luxon": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.2.tgz", - "integrity": "sha512-l5cpE57br4BIjK+9BSkFBOsWtwv6J9bJpC7gdXIzZyI0vuKvNTk0wZZrkQxMGsUAuGW9+WMNWF2IJMD7br2yeQ==" - }, "@types/node": { "version": "22.13.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", @@ -8832,6 +8901,11 @@ "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "peer": true }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" + }, "ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -8919,6 +8993,11 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==" + }, "console-table-printer": { "version": "2.12.1", "resolved": "https://registry.npmjs.org/console-table-printer/-/console-table-printer-2.12.1.tgz", @@ -8968,6 +9047,11 @@ "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-5.0.3.tgz", "integrity": "sha512-Kvr6HmPXUMerlLcLF+Pwq3K7apHpYmGDVqrxcDasBg86UcKeTSNWbEzU8bwdXnxnR44FtMhJAxI4Bov6Y/KUfA==" }, + "dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" + }, "debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -9044,6 +9128,7 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "peer": true, "requires": { "is-arrayish": "^0.2.1" }, @@ -9051,7 +9136,8 @@ "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "peer": true } } }, @@ -9245,6 +9331,16 @@ "jest-util": "^29.7.0" } }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, "fast-content-type-parse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz", @@ -9317,21 +9413,6 @@ "bser": "2.1.1" } }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "requires": { - "escape-string-regexp": "^1.0.5" - }, - "dependencies": { - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - } - } - }, "file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -9429,9 +9510,9 @@ } }, "fs-extra": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", - "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", "requires": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -9439,9 +9520,9 @@ }, "dependencies": { "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==" } } }, @@ -9500,15 +9581,16 @@ } }, "glob": { - "version": "10.3.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.4.tgz", - "integrity": "sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.2.tgz", + "integrity": "sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==", "requires": { "foreground-child": "^3.1.0", - "jackspeak": "^2.0.3", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" }, "dependencies": { "brace-expansion": { @@ -9520,9 +9602,9 @@ } }, "minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", "requires": { "brace-expansion": "^2.0.1" } @@ -9603,6 +9685,14 @@ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "peer": true }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, "ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -9838,12 +9928,11 @@ } }, "jackspeak": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.3.tgz", - "integrity": "sha512-R2bUw+kVZFS/h1AZqBKrSgDmdmjApzgY0AlCPumopFiAlbUxE2gf+SCuBzQ0cP5hHmUmFYF5yw55T97Th5Kstg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz", + "integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==", "requires": { - "@isaacs/cliui": "^8.0.2", - "@pkgjs/parseargs": "^0.11.0" + "@isaacs/cliui": "^8.0.2" } }, "jake": { @@ -10344,11 +10433,6 @@ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" - }, "json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -10380,9 +10464,9 @@ }, "dependencies": { "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==" } } }, @@ -10421,33 +10505,6 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "peer": true }, - "load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - }, - "dependencies": { - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" - } - } - }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -10480,11 +10537,6 @@ "yallist": "^3.0.2" } }, - "luxon": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.3.tgz", - "integrity": "sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg==" - }, "m3u-linter": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/m3u-linter/-/m3u-linter-0.4.2.tgz", @@ -10496,61 +10548,10 @@ "is-valid-path": "^0.1.1" }, "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "requires": { - "balanced-match": "^1.0.0" - } - }, "commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" - }, - "glob": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", - "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", - "requires": { - "foreground-child": "^3.1.0", - "jackspeak": "^4.0.1", - "minimatch": "^10.0.0", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - } - }, - "jackspeak": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", - "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==", - "requires": { - "@isaacs/cliui": "^8.0.2" - } - }, - "lru-cache": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", - "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==" - }, - "minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", - "requires": { - "brace-expansion": "^2.0.1" - } - }, - "path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", - "requires": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - } } } }, @@ -10685,14 +10686,14 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" }, "natural-orderby": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/natural-orderby/-/natural-orderby-3.0.2.tgz", - "integrity": "sha512-x7ZdOwBxZCEm9MM7+eQCjkrNLrW3rkBKNHVr78zbtqnMGVNlnDi6C/eUEYgxHNrcbu0ymvjzcwIL/6H1iHri9g==" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/natural-orderby/-/natural-orderby-5.0.0.tgz", + "integrity": "sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg==" }, - "node-gzip": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/node-gzip/-/node-gzip-1.1.2.tgz", - "integrity": "sha512-ZB6zWpfZHGtxZnPMrJSKHVPrRjURoUzaDbLFj3VO70mpLTW5np96vXyHwft4Id0o+PYIzgDkBUjIzaNHhQ8srw==" + "node-cleanup": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/node-cleanup/-/node-cleanup-2.1.2.tgz", + "integrity": "sha512-qN8v/s2PAJwGUtr1/hYTpNKlD6Y9rc4p8KSmJXyGdYGZsDGKXrGThikLFP9OCHFeLeEpQzPwiAtdIvBLqm//Hw==" }, "node-int64": { "version": "0.4.0", @@ -10767,6 +10768,11 @@ "word-wrap": "^1.2.5" } }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==" + }, "outvariant": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", @@ -10799,6 +10805,11 @@ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" }, + "pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -10842,18 +10853,18 @@ "peer": true }, "path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", "requires": { - "lru-cache": "^9.1.1 || ^10.0.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "dependencies": { "lru-cache": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", - "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==" + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", + "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==" } } }, @@ -10872,71 +10883,12 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" - }, "pirates": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", "peer": true }, - "pkg-conf": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-2.1.0.tgz", - "integrity": "sha1-ISZRTKbyq/69FoWW3xi6V4Z/AFg=", - "requires": { - "find-up": "^2.0.0", - "load-json-file": "^4.0.0" - }, - "dependencies": { - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "requires": { - "locate-path": "^2.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" - } - } - }, "pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -11133,10 +11085,10 @@ "queue-microtask": "^1.2.2" } }, - "run-script-os": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/run-script-os/-/run-script-os-1.1.6.tgz", - "integrity": "sha512-ql6P2LzhBTTDfzKts+Qo4H94VUKpxKDFz6QxxwaUZN0mwvi7L3lpOI7BqPCq7lgDh3XLl0dpeXwfcVIitlrYrw==" + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "semver": { "version": "7.6.3", @@ -11162,67 +11114,6 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "peer": true }, - "signale": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/signale/-/signale-1.4.0.tgz", - "integrity": "sha512-iuh+gPf28RkltuJC7W5MRi6XAjTDCAPC/prJUpQoG4vIP3MJZ+GTydVnodXA7pwvTKb2cA0m9OFZW/cdWy/I/w==", - "requires": { - "chalk": "^2.3.2", - "figures": "^2.0.0", - "pkg-conf": "^2.1.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, "simple-wcswidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.0.1.tgz", @@ -11414,6 +11305,19 @@ } } }, + "timer-node": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/timer-node/-/timer-node-5.0.9.tgz", + "integrity": "sha512-zXxCE/5/YDi0hY9pygqgRqjRbrFRzigYxOudG0I3syaqAAmX9/w9sxex1bNFCN6c1S66RwPtEIJv65dN+1psew==" + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "requires": { + "os-tmpdir": "~1.0.2" + } + }, "tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -11439,14 +11343,6 @@ "url-parse": "^1.5.3" } }, - "transliteration": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/transliteration/-/transliteration-2.3.5.tgz", - "integrity": "sha512-HAGI4Lq4Q9dZ3Utu2phaWgtm3vB6PkLUFqWAScg/UW+1eZ/Tg6Exo4oC0/3VUol/w4BlefLhUUSVBr/9/ZGQOw==", - "requires": { - "yargs": "^17.5.1" - } - }, "ts-api-utils": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", diff --git a/package.json b/package.json index d3aec1ea8..8b957e01c 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "playlist:validate": "tsx scripts/commands/playlist/validate.ts", "playlist:lint": "npx m3u-linter -c m3u-linter.json", "playlist:test": "tsx scripts/commands/playlist/test.ts", + "playlist:edit": "tsx scripts/commands/playlist/edit.ts", "playlist:deploy": "npx gh-pages-clean && npx gh-pages -m \"Deploy to GitHub Pages\" -d .gh-pages -r https://$GITHUB_TOKEN@github.com/iptv-org/iptv.git", "readme:update": "tsx scripts/commands/readme/update.ts", "report:create": "tsx scripts/commands/report/create.ts", @@ -39,12 +40,15 @@ "dependencies": { "@eslint/eslintrc": "^3.3.0", "@eslint/js": "^9.21.0", - "@freearhey/core": "^0.7.0", + "@freearhey/core": "^0.8.2", + "@freearhey/search-js": "^0.1.2", + "@inquirer/prompts": "^7.4.1", "@octokit/core": "^6.1.4", "@octokit/plugin-paginate-rest": "^11.4.3", "@octokit/plugin-rest-endpoint-methods": "^7.1.3", "@octokit/types": "^11.1.0", "@types/cli-progress": "^3.11.3", + "@types/fs-extra": "^11.0.4", "@types/jest": "^29.5.14", "@types/lodash": "^4.14.198", "@types/numeral": "^2.0.3", @@ -58,6 +62,7 @@ "commander": "^8.3.0", "console-table-printer": "^2.12.1", "eslint": "^9.17.0", + "glob": "^11.0.2", "globals": "^16.0.0", "iptv-checker": "^0.29.1", "iptv-playlist-parser": "^0.13.0", @@ -65,8 +70,8 @@ "lodash": "^4.17.21", "m3u-linter": "^0.4.2", "markdown-include": "^0.4.3", + "node-cleanup": "^2.1.2", "numeral": "^2.0.6", - "transliteration": "^2.3.5", "ts-jest": "^29.2.5", "tsx": "^4.6.2", "valid-url": "^1.0.9" diff --git a/scripts/commands/api/generate.ts b/scripts/commands/api/generate.ts index 670fa091d..f264260f9 100644 --- a/scripts/commands/api/generate.ts +++ b/scripts/commands/api/generate.ts @@ -1,30 +1,25 @@ -import { Logger, Storage, Collection } from '@freearhey/core' +import { DataLoader, DataProcessor, PlaylistParser } from '../../core' +import type { DataProcessorData } from '../../types/dataProcessor' import { API_DIR, STREAMS_DIR, DATA_DIR } from '../../constants' -import { PlaylistParser } from '../../core' -import { Stream, Channel, Feed } from '../../models' -import { uniqueId } from 'lodash' +import type { DataLoaderData } from '../../types/dataLoader' +import { Logger, Storage } from '@freearhey/core' +import { Stream } from '../../models' async function main() { const logger = new Logger() - logger.info('loading api data...') + logger.info('loading data from api...') + const processor = new DataProcessor() const dataStorage = new Storage(DATA_DIR) - const channelsData = await dataStorage.json('channels.json') - const channels = new Collection(channelsData).map(data => new Channel(data)) - const channelsGroupedById = channels.keyBy((channel: Channel) => channel.id) - const feedsData = await dataStorage.json('feeds.json') - const feeds = new Collection(feedsData).map(data => - new Feed(data).withChannel(channelsGroupedById) - ) - const feedsGroupedByChannelId = feeds.groupBy((feed: Feed) => - feed.channel ? feed.channel.id : uniqueId() - ) + const dataLoader = new DataLoader({ storage: dataStorage }) + const data: DataLoaderData = await dataLoader.load() + const { channelsKeyById, feedsGroupedByChannelId }: DataProcessorData = processor.process(data) logger.info('loading streams...') const streamsStorage = new Storage(STREAMS_DIR) const parser = new PlaylistParser({ storage: streamsStorage, - channelsGroupedById, + channelsKeyById, feedsGroupedByChannelId }) const files = await streamsStorage.list('**/*.m3u') diff --git a/scripts/commands/api/load.ts b/scripts/commands/api/load.ts index fbb1fea43..3fdc70043 100644 --- a/scripts/commands/api/load.ts +++ b/scripts/commands/api/load.ts @@ -1,23 +1,24 @@ -import { Logger } from '@freearhey/core' -import { ApiClient } from '../../core' +import { DATA_DIR } from '../../constants' +import { Storage } from '@freearhey/core' +import { DataLoader } from '../../core' async function main() { - const logger = new Logger() - const client = new ApiClient({ logger }) + const storage = new Storage(DATA_DIR) + const loader = new DataLoader({ storage }) - const requests = [ - client.download('blocklist.json'), - client.download('categories.json'), - client.download('channels.json'), - client.download('countries.json'), - client.download('languages.json'), - client.download('regions.json'), - client.download('subdivisions.json'), - client.download('feeds.json'), - client.download('timezones.json') - ] - - await Promise.all(requests) + await Promise.all([ + loader.download('blocklist.json'), + loader.download('categories.json'), + loader.download('channels.json'), + loader.download('countries.json'), + loader.download('languages.json'), + loader.download('regions.json'), + loader.download('subdivisions.json'), + loader.download('feeds.json'), + loader.download('timezones.json'), + loader.download('guides.json'), + loader.download('streams.json') + ]) } main() diff --git a/scripts/commands/playlist/edit.ts b/scripts/commands/playlist/edit.ts new file mode 100644 index 000000000..d87590b1f --- /dev/null +++ b/scripts/commands/playlist/edit.ts @@ -0,0 +1,208 @@ +import { Storage, Collection, Logger, Dictionary } from '@freearhey/core' +import { DataLoader, DataProcessor, PlaylistParser } from '../../core' +import type { ChannelSearchableData } from '../../types/channel' +import { Channel, Feed, Playlist, Stream } from '../../models' +import { DataProcessorData } from '../../types/dataProcessor' +import { DataLoaderData } from '../../types/dataLoader' +import { select, input } from '@inquirer/prompts' +import { DATA_DIR } from '../../constants' +import nodeCleanup from 'node-cleanup' +import sjs from '@freearhey/search-js' +import { Command } from 'commander' +import readline from 'readline' + +type ChoiceValue = { type: string; value?: Feed | Channel } +type Choice = { name: string; short?: string; value: ChoiceValue; default?: boolean } + +if (process.platform === 'win32') { + readline + .createInterface({ + input: process.stdin, + output: process.stdout + }) + .on('SIGINT', function () { + process.emit('SIGINT') + }) +} + +const program = new Command() + +program.argument('', 'Path to *.channels.xml file to edit').parse(process.argv) + +const filepath = program.args[0] +const logger = new Logger() +const storage = new Storage() +let parsedStreams = new Collection() + +main(filepath) +nodeCleanup(() => { + save(filepath) +}) + +export default async function main(filepath: string) { + if (!(await storage.exists(filepath))) { + throw new Error(`File "${filepath}" does not exists`) + } + + logger.info('loading data from api...') + const processor = new DataProcessor() + const dataStorage = new Storage(DATA_DIR) + const loader = new DataLoader({ storage: dataStorage }) + const data: DataLoaderData = await loader.load() + const { channels, channelsKeyById, feedsGroupedByChannelId }: DataProcessorData = + processor.process(data) + + logger.info('loading streams...') + const parser = new PlaylistParser({ storage, feedsGroupedByChannelId, channelsKeyById }) + parsedStreams = await parser.parseFile(filepath) + const streamsWithoutId = parsedStreams.filter((stream: Stream) => !stream.id) + + logger.info( + `found ${parsedStreams.count()} streams (including ${streamsWithoutId.count()} without ID)` + ) + + logger.info('creating search index...') + const items = channels.map((channel: Channel) => channel.getSearchable()).all() + const searchIndex = sjs.createIndex(items, { + searchable: ['name', 'altNames', 'guideNames', 'streamNames', 'feedFullNames'] + }) + + logger.info('starting...\n') + + for (const stream of streamsWithoutId.all()) { + try { + stream.id = await selectChannel(stream, searchIndex, feedsGroupedByChannelId, channelsKeyById) + } catch (err) { + logger.info(err.message) + break + } + } + + streamsWithoutId.forEach((stream: Stream) => { + if (stream.id === '-') { + stream.id = '' + } + }) +} + +async function selectChannel( + stream: Stream, + searchIndex, + feedsGroupedByChannelId: Dictionary, + channelsKeyById: Dictionary +): Promise { + const query = escapeRegex(stream.getName()) + const similarChannels = searchIndex + .search(query) + .map((item: ChannelSearchableData) => channelsKeyById.get(item.id)) + + const url = stream.url.length > 50 ? stream.url.slice(0, 50) + '...' : stream.url + + const selected: ChoiceValue = await select({ + message: `Select channel ID for "${stream.name}" (${url}):`, + choices: getChannelChoises(new Collection(similarChannels)), + pageSize: 10 + }) + + switch (selected.type) { + case 'skip': + return '-' + case 'type': { + const typedChannelId = await input({ message: ' Channel ID:' }) + if (!typedChannelId) return '' + const selectedFeedId = await selectFeed(typedChannelId, feedsGroupedByChannelId) + if (selectedFeedId === '-') return typedChannelId + return [typedChannelId, selectedFeedId].join('@') + } + case 'channel': { + const selectedChannel = selected.value + if (!selectedChannel) return '' + const selectedFeedId = await selectFeed(selectedChannel.id, feedsGroupedByChannelId) + if (selectedFeedId === '-') return selectedChannel.id + return [selectedChannel.id, selectedFeedId].join('@') + } + } + + return '' +} + +async function selectFeed(channelId: string, feedsGroupedByChannelId: Dictionary): Promise { + const channelFeeds = new Collection(feedsGroupedByChannelId.get(channelId)) || new Collection() + const choices = getFeedChoises(channelFeeds) + + const selected: ChoiceValue = await select({ + message: `Select feed ID for "${channelId}":`, + choices, + pageSize: 10 + }) + + switch (selected.type) { + case 'skip': + return '-' + case 'type': + return await input({ message: ' Feed ID:', default: 'SD' }) + case 'feed': + const selectedFeed = selected.value + if (!selectedFeed) return '' + return selectedFeed.id + } + + return '' +} + +function getChannelChoises(channels: Collection): Choice[] { + const choises: Choice[] = [] + + channels.forEach((channel: Channel) => { + const names = new Collection([channel.name, ...channel.altNames.all()]).uniq().join(', ') + + choises.push({ + value: { + type: 'channel', + value: channel + }, + name: `${channel.id} (${names})`, + short: `${channel.id}` + }) + }) + + choises.push({ name: 'Type...', value: { type: 'type' } }) + choises.push({ name: 'Skip', value: { type: 'skip' } }) + + return choises +} + +function getFeedChoises(feeds: Collection): Choice[] { + const choises: Choice[] = [] + + feeds.forEach((feed: Feed) => { + let name = `${feed.id} (${feed.name})` + if (feed.isMain) name += ' [main]' + + choises.push({ + value: { + type: 'feed', + value: feed + }, + default: feed.isMain, + name, + short: feed.id + }) + }) + + choises.push({ name: 'Type...', value: { type: 'type' } }) + choises.push({ name: 'Skip', value: { type: 'skip' } }) + + return choises +} + +function save(filepath: string) { + if (!storage.existsSync(filepath)) return + const playlist = new Playlist(parsedStreams) + storage.saveSync(filepath, playlist.toString()) + logger.info(`\nFile '${filepath}' successfully saved`) +} + +function escapeRegex(string: string) { + return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&') +} diff --git a/scripts/commands/playlist/format.ts b/scripts/commands/playlist/format.ts index 6ac14cb62..43868b73e 100644 --- a/scripts/commands/playlist/format.ts +++ b/scripts/commands/playlist/format.ts @@ -1,33 +1,28 @@ -import { Logger, Storage, Collection } from '@freearhey/core' +import { Logger, Storage } from '@freearhey/core' import { STREAMS_DIR, DATA_DIR } from '../../constants' -import { PlaylistParser } from '../../core' -import { Stream, Playlist, Channel, Feed } from '../../models' +import { DataLoader, DataProcessor, PlaylistParser } from '../../core' +import { Stream, Playlist } from '../../models' import { program } from 'commander' -import { uniqueId } from 'lodash' +import { DataLoaderData } from '../../types/dataLoader' +import { DataProcessorData } from '../../types/dataProcessor' program.argument('[filepath]', 'Path to file to validate').parse(process.argv) async function main() { - const streamsStorage = new Storage(STREAMS_DIR) const logger = new Logger() logger.info('loading data from api...') + const processor = new DataProcessor() const dataStorage = new Storage(DATA_DIR) - const channelsData = await dataStorage.json('channels.json') - const channels = new Collection(channelsData).map(data => new Channel(data)) - const channelsGroupedById = channels.keyBy((channel: Channel) => channel.id) - const feedsData = await dataStorage.json('feeds.json') - const feeds = new Collection(feedsData).map(data => - new Feed(data).withChannel(channelsGroupedById) - ) - const feedsGroupedByChannelId = feeds.groupBy(feed => - feed.channel ? feed.channel.id : uniqueId() - ) + const loader = new DataLoader({ storage: dataStorage }) + const data: DataLoaderData = await loader.load() + const { channelsKeyById, feedsGroupedByChannelId }: DataProcessorData = processor.process(data) logger.info('loading streams...') + const streamsStorage = new Storage(STREAMS_DIR) const parser = new PlaylistParser({ storage: streamsStorage, - channelsGroupedById, + channelsKeyById, feedsGroupedByChannelId }) const files = program.args.length ? program.args : await streamsStorage.list('**/*.m3u') @@ -46,7 +41,7 @@ async function main() { logger.info('removing wrong id...') streams = streams.map((stream: Stream) => { - if (!stream.channel || channelsGroupedById.missing(stream.channel.id)) { + if (!stream.channel || channelsKeyById.missing(stream.channel.id)) { stream.id = '' } diff --git a/scripts/commands/playlist/generate.ts b/scripts/commands/playlist/generate.ts index 7acbbba4b..b903b5a43 100644 --- a/scripts/commands/playlist/generate.ts +++ b/scripts/commands/playlist/generate.ts @@ -1,109 +1,47 @@ -import { Logger, Storage, Collection } from '@freearhey/core' -import { PlaylistParser } from '../../core' -import { - Stream, - Category, - Channel, - Language, - Country, - Region, - Subdivision, - Feed, - Timezone -} from '../../models' +import { PlaylistParser, DataProcessor, DataLoader } from '../../core' +import type { DataProcessorData } from '../../types/dataProcessor' +import { DATA_DIR, LOGS_DIR, STREAMS_DIR } from '../../constants' +import type { DataLoaderData } from '../../types/dataLoader' +import { Logger, Storage, File } from '@freearhey/core' +import { Stream } from '../../models' import { uniqueId } from 'lodash' import { + IndexCategoryGenerator, + IndexLanguageGenerator, + IndexCountryGenerator, + IndexRegionGenerator, CategoriesGenerator, CountriesGenerator, LanguagesGenerator, RegionsGenerator, - IndexGenerator, - IndexCategoryGenerator, - IndexCountryGenerator, - IndexLanguageGenerator, - IndexRegionGenerator + IndexGenerator } from '../../generators' -import { DATA_DIR, LOGS_DIR, STREAMS_DIR } from '../../constants' async function main() { const logger = new Logger() - const dataStorage = new Storage(DATA_DIR) - const generatorsLogger = new Logger({ - stream: await new Storage(LOGS_DIR).createStream(`generators.log`) - }) + const logFile = new File('generators.log') logger.info('loading data from api...') - const categoriesData = await dataStorage.json('categories.json') - const countriesData = await dataStorage.json('countries.json') - const languagesData = await dataStorage.json('languages.json') - const regionsData = await dataStorage.json('regions.json') - const subdivisionsData = await dataStorage.json('subdivisions.json') - const timezonesData = await dataStorage.json('timezones.json') - const channelsData = await dataStorage.json('channels.json') - const feedsData = await dataStorage.json('feeds.json') - - logger.info('preparing data...') - const subdivisions = new Collection(subdivisionsData).map(data => new Subdivision(data)) - const subdivisionsGroupedByCode = subdivisions.keyBy( - (subdivision: Subdivision) => subdivision.code - ) - const subdivisionsGroupedByCountryCode = subdivisions.groupBy( - (subdivision: Subdivision) => subdivision.countryCode - ) - let regions = new Collection(regionsData).map(data => - new Region(data).withSubdivisions(subdivisions) - ) - const regionsGroupedByCode = regions.keyBy((region: Region) => region.code) - const categories = new Collection(categoriesData).map(data => new Category(data)) - const categoriesGroupedById = categories.keyBy((category: Category) => category.id) - const languages = new Collection(languagesData).map(data => new Language(data)) - const languagesGroupedByCode = languages.keyBy((language: Language) => language.code) - const countries = new Collection(countriesData).map(data => - new Country(data) - .withRegions(regions) - .withLanguage(languagesGroupedByCode) - .withSubdivisions(subdivisionsGroupedByCountryCode) - ) - const countriesGroupedByCode = countries.keyBy((country: Country) => country.code) - regions = regions.map((region: Region) => region.withCountries(countriesGroupedByCode)) - - const timezones = new Collection(timezonesData).map(data => - new Timezone(data).withCountries(countriesGroupedByCode) - ) - const timezonesGroupedById = timezones.keyBy((timezone: Timezone) => timezone.id) - - const channels = new Collection(channelsData).map(data => - new Channel(data) - .withCategories(categoriesGroupedById) - .withCountry(countriesGroupedByCode) - .withSubdivision(subdivisionsGroupedByCode) - ) - const channelsGroupedById = channels.keyBy((channel: Channel) => channel.id) - const feeds = new Collection(feedsData).map(data => - new Feed(data) - .withChannel(channelsGroupedById) - .withLanguages(languagesGroupedByCode) - .withTimezones(timezonesGroupedById) - .withBroadcastCountries( - countriesGroupedByCode, - regionsGroupedByCode, - subdivisionsGroupedByCode - ) - .withBroadcastRegions(regions) - .withBroadcastSubdivisions(subdivisionsGroupedByCode) - ) - const feedsGroupedByChannelId = feeds.groupBy((feed: Feed) => - feed.channel ? feed.channel.id : uniqueId() - ) + const processor = new DataProcessor() + const dataStorage = new Storage(DATA_DIR) + const loader = new DataLoader({ storage: dataStorage }) + const data: DataLoaderData = await loader.load() + const { + feedsGroupedByChannelId, + channelsKeyById, + categories, + countries, + regions + }: DataProcessorData = processor.process(data) logger.info('loading streams...') - const storage = new Storage(STREAMS_DIR) + const streamsStorage = new Storage(STREAMS_DIR) const parser = new PlaylistParser({ - storage, - channelsGroupedById, - feedsGroupedByChannelId + storage: streamsStorage, + feedsGroupedByChannelId, + channelsKeyById }) - const files = await storage.list('**/*.m3u') + const files = await streamsStorage.list('**/*.m3u') let streams = await parser.parse(files) const totalStreams = streams.count() streams = streams.uniqBy((stream: Stream) => @@ -122,42 +60,46 @@ async function main() { ) logger.info('generating categories/...') - await new CategoriesGenerator({ categories, streams, logger: generatorsLogger }).generate() + await new CategoriesGenerator({ categories, streams, logFile }).generate() logger.info('generating countries/...') await new CountriesGenerator({ countries, streams, - logger: generatorsLogger + logFile }).generate() logger.info('generating languages/...') - await new LanguagesGenerator({ streams, logger: generatorsLogger }).generate() + await new LanguagesGenerator({ streams, logFile }).generate() logger.info('generating regions/...') await new RegionsGenerator({ streams, regions, - logger: generatorsLogger + logFile }).generate() logger.info('generating index.m3u...') - await new IndexGenerator({ streams, logger: generatorsLogger }).generate() + await new IndexGenerator({ streams, logFile }).generate() logger.info('generating index.category.m3u...') - await new IndexCategoryGenerator({ streams, logger: generatorsLogger }).generate() + await new IndexCategoryGenerator({ streams, logFile }).generate() logger.info('generating index.country.m3u...') await new IndexCountryGenerator({ streams, - logger: generatorsLogger + logFile }).generate() logger.info('generating index.language.m3u...') - await new IndexLanguageGenerator({ streams, logger: generatorsLogger }).generate() + await new IndexLanguageGenerator({ streams, logFile }).generate() logger.info('generating index.region.m3u...') - await new IndexRegionGenerator({ streams, regions, logger: generatorsLogger }).generate() + await new IndexRegionGenerator({ streams, regions, logFile }).generate() + + logger.info('saving generators.log...') + const logStorage = new Storage(LOGS_DIR) + logStorage.saveFile(logFile) } main() diff --git a/scripts/commands/playlist/test.ts b/scripts/commands/playlist/test.ts index f32f2e0c2..777c19f8d 100644 --- a/scripts/commands/playlist/test.ts +++ b/scripts/commands/playlist/test.ts @@ -1,13 +1,15 @@ import { Logger, Storage, Collection } from '@freearhey/core' import { ROOT_DIR, STREAMS_DIR, DATA_DIR } from '../../constants' -import { PlaylistParser, StreamTester, CliTable } from '../../core' -import { Stream, Feed, Channel } from '../../models' +import { PlaylistParser, StreamTester, CliTable, DataProcessor, DataLoader } from '../../core' +import { Stream } from '../../models' import { program } from 'commander' import { eachLimit } from 'async-es' import commandExists from 'command-exists' import chalk from 'chalk' import os from 'node:os' import dns from 'node:dns' +import type { DataLoaderData } from '../../types/dataLoader' +import type { DataProcessorData } from '../../types/dataProcessor' const cpus = os.cpus() @@ -54,22 +56,18 @@ async function main() { return } - logger.info('loading channels from api...') + logger.info('loading data from api...') + const processor = new DataProcessor() const dataStorage = new Storage(DATA_DIR) - const channelsData = await dataStorage.json('channels.json') - const channels = new Collection(channelsData).map(data => new Channel(data)) - const channelsGroupedById = channels.keyBy((channel: Channel) => channel.id) - const feedsData = await dataStorage.json('feeds.json') - const feeds = new Collection(feedsData).map(data => - new Feed(data).withChannel(channelsGroupedById) - ) - const feedsGroupedByChannelId = feeds.groupBy(feed => feed.channel) + const loader = new DataLoader({ storage: dataStorage }) + const data: DataLoaderData = await loader.load() + const { channelsKeyById, feedsGroupedByChannelId }: DataProcessorData = processor.process(data) logger.info('loading streams...') const rootStorage = new Storage(ROOT_DIR) const parser = new PlaylistParser({ storage: rootStorage, - channelsGroupedById, + channelsKeyById, feedsGroupedByChannelId }) const files = program.args.length ? program.args : await rootStorage.list(`${STREAMS_DIR}/*.m3u`) @@ -156,15 +154,24 @@ function drawTable() { } } -function onFinish() { +function onFinish(error) { clearInterval(interval) + if (error) { + console.error(error) + process.exit(1) + } + drawTable() - logger.error(`\n${errors + warnings} problems (${errors} errors, ${warnings} warnings)`) + if (errors > 0 || warnings > 0) { + console.log( + chalk.red(`\n${errors + warnings} problems (${errors} errors, ${warnings} warnings)`) + ) - if (errors > 0) { - process.exit(1) + if (errors > 0) { + process.exit(1) + } } process.exit(0) diff --git a/scripts/commands/playlist/update.ts b/scripts/commands/playlist/update.ts index b2ac5b814..55551b67d 100644 --- a/scripts/commands/playlist/update.ts +++ b/scripts/commands/playlist/update.ts @@ -1,38 +1,33 @@ +import { DataLoader, DataProcessor, IssueLoader, PlaylistParser } from '../../core' import { Logger, Storage, Collection, Dictionary } from '@freearhey/core' +import type { DataProcessorData } from '../../types/dataProcessor' +import { Stream, Playlist, Channel, Issue } from '../../models' +import type { DataLoaderData } from '../../types/dataLoader' import { DATA_DIR, STREAMS_DIR } from '../../constants' -import { IssueLoader, PlaylistParser } from '../../core' -import { Stream, Playlist, Channel, Feed, Issue } from '../../models' import validUrl from 'valid-url' -import { uniqueId } from 'lodash' let processedIssues = new Collection() async function main() { const logger = new Logger({ disabled: true }) - const loader = new IssueLoader() + const issueLoader = new IssueLoader() logger.info('loading issues...') - const issues = await loader.load() + const issues = await issueLoader.load() - logger.info('loading channels from api...') + logger.info('loading data from api...') + const processor = new DataProcessor() const dataStorage = new Storage(DATA_DIR) - const channelsData = await dataStorage.json('channels.json') - const channels = new Collection(channelsData).map(data => new Channel(data)) - const channelsGroupedById = channels.keyBy((channel: Channel) => channel.id) - const feedsData = await dataStorage.json('feeds.json') - const feeds = new Collection(feedsData).map(data => - new Feed(data).withChannel(channelsGroupedById) - ) - const feedsGroupedByChannelId = feeds.groupBy((feed: Feed) => - feed.channel ? feed.channel.id : uniqueId() - ) + const dataLoader = new DataLoader({ storage: dataStorage }) + const data: DataLoaderData = await dataLoader.load() + const { channelsKeyById, feedsGroupedByChannelId }: DataProcessorData = processor.process(data) logger.info('loading streams...') const streamsStorage = new Storage(STREAMS_DIR) const parser = new PlaylistParser({ storage: streamsStorage, feedsGroupedByChannelId, - channelsGroupedById + channelsKeyById }) const files = await streamsStorage.list('**/*.m3u') const streams = await parser.parse(files) @@ -44,7 +39,7 @@ async function main() { await editStreams({ streams, issues, - channelsGroupedById, + channelsKeyById, feedsGroupedByChannelId }) @@ -52,7 +47,7 @@ async function main() { await addStreams({ streams, issues, - channelsGroupedById, + channelsKeyById, feedsGroupedByChannelId }) @@ -101,12 +96,12 @@ async function removeStreams({ streams, issues }: { streams: Collection; issues: async function editStreams({ streams, issues, - channelsGroupedById, + channelsKeyById, feedsGroupedByChannelId }: { streams: Collection issues: Collection - channelsGroupedById: Dictionary + channelsKeyById: Dictionary feedsGroupedByChannelId: Dictionary }) { const requests = issues.filter( @@ -129,7 +124,7 @@ async function editStreams({ stream .setChannelId(channelId) .setFeedId(feedId) - .withChannel(channelsGroupedById) + .withChannel(channelsKeyById) .withFeed(feedsGroupedByChannelId) .updateId() .updateName() @@ -143,8 +138,8 @@ async function editStreams({ if (data.has('label')) stream.setLabel(label) if (data.has('quality')) stream.setQuality(quality) - if (data.has('httpUserAgent')) stream.setHttpUserAgent(httpUserAgent) - if (data.has('httpReferrer')) stream.setHttpReferrer(httpReferrer) + if (data.has('httpUserAgent')) stream.setUserAgent(httpUserAgent) + if (data.has('httpReferrer')) stream.setReferrer(httpReferrer) processedIssues.add(issue.number) }) @@ -153,12 +148,12 @@ async function editStreams({ async function addStreams({ streams, issues, - channelsGroupedById, + channelsKeyById, feedsGroupedByChannelId }: { streams: Collection issues: Collection - channelsGroupedById: Dictionary + channelsKeyById: Dictionary feedsGroupedByChannelId: Dictionary }) { const requests = issues.filter( @@ -168,51 +163,32 @@ async function addStreams({ const data = issue.data if (data.missing('streamId') || data.missing('streamUrl')) return if (streams.includes((_stream: Stream) => _stream.url === data.getString('streamUrl'))) return - const stringUrl = data.getString('streamUrl') || '' - if (!isUri(stringUrl)) return + const streamUrl = data.getString('streamUrl') || '' + if (!isUri(streamUrl)) return const streamId = data.getString('streamId') || '' - const [channelId] = streamId.split('@') + const [channelId, feedId] = streamId.split('@') - const channel: Channel = channelsGroupedById.get(channelId) + const channel: Channel = channelsKeyById.get(channelId) if (!channel) return - const label = data.getString('label') || '' - const quality = data.getString('quality') || '' - const httpUserAgent = data.getString('httpUserAgent') || '' - const httpReferrer = data.getString('httpReferrer') || '' + const label = data.getString('label') || null + const quality = data.getString('quality') || null + const httpUserAgent = data.getString('httpUserAgent') || null + const httpReferrer = data.getString('httpReferrer') || null const stream = new Stream({ - tvg: { - id: streamId, - name: '', - url: '', - logo: '', - rec: '', - shift: '' - }, + channel: channelId, + feed: feedId, name: data.getString('channelName') || channel.name, - url: stringUrl, - group: { - title: '' - }, - http: { - 'user-agent': httpUserAgent, - referrer: httpReferrer - }, - line: -1, - raw: '', - timeshift: '', - catchup: { - type: '', - source: '', - days: '' - } + url: streamUrl, + user_agent: httpUserAgent, + referrer: httpReferrer, + quality, + label }) - .withChannel(channelsGroupedById) + .withChannel(channelsKeyById) .withFeed(feedsGroupedByChannelId) - .setLabel(label) - .setQuality(quality) .updateName() .updateFilepath() diff --git a/scripts/commands/playlist/validate.ts b/scripts/commands/playlist/validate.ts index 6296b5651..f1f7742eb 100644 --- a/scripts/commands/playlist/validate.ts +++ b/scripts/commands/playlist/validate.ts @@ -1,10 +1,11 @@ import { Logger, Storage, Collection, Dictionary } from '@freearhey/core' -import { PlaylistParser } from '../../core' -import { Channel, Stream, Blocked, Feed } from '../../models' +import { DataLoader, DataProcessor, PlaylistParser } from '../../core' +import { DataProcessorData } from '../../types/dataProcessor' +import { DATA_DIR, STREAMS_DIR } from '../../constants' +import { DataLoaderData } from '../../types/dataLoader' +import { BlocklistRecord, Stream } from '../../models' import { program } from 'commander' import chalk from 'chalk' -import { uniqueId } from 'lodash' -import { DATA_DIR, STREAMS_DIR } from '../../constants' program.argument('[filepath]', 'Path to file to validate').parse(process.argv) @@ -18,26 +19,21 @@ async function main() { const logger = new Logger() logger.info('loading data from api...') + const processor = new DataProcessor() const dataStorage = new Storage(DATA_DIR) - const channelsData = await dataStorage.json('channels.json') - const channels = new Collection(channelsData).map(data => new Channel(data)) - const channelsGroupedById = channels.keyBy((channel: Channel) => channel.id) - const feedsData = await dataStorage.json('feeds.json') - const feeds = new Collection(feedsData).map(data => - new Feed(data).withChannel(channelsGroupedById) - ) - const feedsGroupedByChannelId = feeds.groupBy((feed: Feed) => - feed.channel ? feed.channel.id : uniqueId() - ) - const blocklistContent = await dataStorage.json('blocklist.json') - const blocklist = new Collection(blocklistContent).map(data => new Blocked(data)) - const blocklistGroupedByChannelId = blocklist.keyBy((blocked: Blocked) => blocked.channelId) + const loader = new DataLoader({ storage: dataStorage }) + const data: DataLoaderData = await loader.load() + const { + channelsKeyById, + feedsGroupedByChannelId, + blocklistRecordsGroupedByChannelId + }: DataProcessorData = processor.process(data) logger.info('loading streams...') const streamsStorage = new Storage(STREAMS_DIR) const parser = new PlaylistParser({ storage: streamsStorage, - channelsGroupedById, + channelsKeyById, feedsGroupedByChannelId }) const files = program.args.length ? program.args : await streamsStorage.list('**/*.m3u') @@ -55,11 +51,11 @@ async function main() { const buffer = new Dictionary() streams.forEach((stream: Stream) => { if (stream.channelId) { - const channel = channelsGroupedById.get(stream.channelId) + const channel = channelsKeyById.get(stream.channelId) if (!channel) { log.add({ type: 'warning', - line: stream.line, + line: stream.getLine(), message: `"${stream.id}" is not in the database` }) } @@ -69,40 +65,43 @@ async function main() { if (duplicate) { log.add({ type: 'warning', - line: stream.line, + line: stream.getLine(), message: `"${stream.url}" is already on the playlist` }) } else { buffer.set(stream.url, true) } - const blocked = stream.channel ? blocklistGroupedByChannelId.get(stream.channel.id) : false - if (blocked) { - if (blocked.reason === 'dmca') { + const blocklistRecords = stream.channel + ? new Collection(blocklistRecordsGroupedByChannelId.get(stream.channel.id)) + : new Collection() + + blocklistRecords.forEach((blocklistRecord: BlocklistRecord) => { + if (blocklistRecord.reason === 'dmca') { log.add({ type: 'error', - line: stream.line, - message: `"${blocked.channelId}" is on the blocklist due to claims of copyright holders (${blocked.ref})` + line: stream.getLine(), + message: `"${blocklistRecord.channelId}" is on the blocklist due to claims of copyright holders (${blocklistRecord.ref})` }) - } else if (blocked.reason === 'nsfw') { + } else if (blocklistRecord.reason === 'nsfw') { log.add({ type: 'error', - line: stream.line, - message: `"${blocked.channelId}" is on the blocklist due to NSFW content (${blocked.ref})` + line: stream.getLine(), + message: `"${blocklistRecord.channelId}" is on the blocklist due to NSFW content (${blocklistRecord.ref})` }) } - } + }) }) if (log.notEmpty()) { - logger.info(`\n${chalk.underline(filepath)}`) + console.log(`\n${chalk.underline(filepath)}`) log.forEach((logItem: LogItem) => { const position = logItem.line.toString().padEnd(6, ' ') const type = logItem.type.padEnd(9, ' ') const status = logItem.type === 'error' ? chalk.red(type) : chalk.yellow(type) - logger.info(` ${chalk.gray(position)}${status}${logItem.message}`) + console.log(` ${chalk.gray(position)}${status}${logItem.message}`) }) errors = errors.concat(log.filter((logItem: LogItem) => logItem.type === 'error')) @@ -110,16 +109,18 @@ async function main() { } } - logger.error( - chalk.red( - `\n${ - errors.count() + warnings.count() - } problems (${errors.count()} errors, ${warnings.count()} warnings)` + if (errors.count() || warnings.count()) { + console.log( + chalk.red( + `\n${ + errors.count() + warnings.count() + } problems (${errors.count()} errors, ${warnings.count()} warnings)` + ) ) - ) - if (errors.count()) { - process.exit(1) + if (errors.count()) { + process.exit(1) + } } } diff --git a/scripts/commands/report/create.ts b/scripts/commands/report/create.ts index 7584105d1..4ad272de3 100644 --- a/scripts/commands/report/create.ts +++ b/scripts/commands/report/create.ts @@ -1,44 +1,41 @@ +import { DataLoader, DataProcessor, IssueLoader, PlaylistParser } from '../../core' import { Logger, Storage, Collection, Dictionary } from '@freearhey/core' +import { DataProcessorData } from '../../types/dataProcessor' import { DATA_DIR, STREAMS_DIR } from '../../constants' -import { IssueLoader, PlaylistParser } from '../../core' -import { Blocked, Channel, Issue, Stream, Feed } from '../../models' -import { uniqueId } from 'lodash' +import { DataLoaderData } from '../../types/dataLoader' +import { Issue, Stream } from '../../models' async function main() { const logger = new Logger() - const loader = new IssueLoader() + const issueLoader = new IssueLoader() let report = new Collection() logger.info('loading issues...') - const issues = await loader.load() + const issues = await issueLoader.load() logger.info('loading data from api...') + const processor = new DataProcessor() const dataStorage = new Storage(DATA_DIR) - const channelsData = await dataStorage.json('channels.json') - const channels = new Collection(channelsData).map(data => new Channel(data)) - const channelsGroupedById = channels.keyBy((channel: Channel) => channel.id) - const feedsData = await dataStorage.json('feeds.json') - const feeds = new Collection(feedsData).map(data => - new Feed(data).withChannel(channelsGroupedById) - ) - const feedsGroupedByChannelId = feeds.groupBy((feed: Feed) => - feed.channel ? feed.channel.id : uniqueId() - ) - const blocklistContent = await dataStorage.json('blocklist.json') - const blocklist = new Collection(blocklistContent).map(data => new Blocked(data)) - const blocklistGroupedByChannelId = blocklist.keyBy((blocked: Blocked) => blocked.channelId) + const dataLoader = new DataLoader({ storage: dataStorage }) + const data: DataLoaderData = await dataLoader.load() + const { + channelsKeyById, + feedsGroupedByChannelId, + blocklistRecordsGroupedByChannelId + }: DataProcessorData = processor.process(data) logger.info('loading streams...') const streamsStorage = new Storage(STREAMS_DIR) const parser = new PlaylistParser({ storage: streamsStorage, - channelsGroupedById, + channelsKeyById, feedsGroupedByChannelId }) const files = await streamsStorage.list('**/*.m3u') const streams = await parser.parse(files) const streamsGroupedByUrl = streams.groupBy((stream: Stream) => stream.url) const streamsGroupedByChannelId = streams.groupBy((stream: Stream) => stream.channelId) + const streamsGroupedById = streams.groupBy((stream: Stream) => stream.getId()) logger.info('checking broken streams reports...') const brokenStreamReports = issues.filter(issue => @@ -94,8 +91,8 @@ async function main() { if (!channelId) result.status = 'missing_id' else if (!streamUrl) result.status = 'missing_link' - else if (blocklistGroupedByChannelId.has(channelId)) result.status = 'blocked' - else if (channelsGroupedById.missing(channelId)) result.status = 'wrong_id' + else if (blocklistRecordsGroupedByChannelId.has(channelId)) result.status = 'blocked' + else if (channelsKeyById.missing(channelId)) result.status = 'wrong_id' else if (streamsGroupedByUrl.has(streamUrl)) result.status = 'on_playlist' else if (addRequestsBuffer.has(streamUrl)) result.status = 'duplicate' else result.status = 'pending' @@ -124,7 +121,7 @@ async function main() { if (!streamUrl) result.status = 'missing_link' else if (streamsGroupedByUrl.missing(streamUrl)) result.status = 'invalid_link' - else if (channelId && channelsGroupedById.missing(channelId)) result.status = 'invalid_id' + else if (channelId && channelsKeyById.missing(channelId)) result.status = 'invalid_id' report.add(result) }) @@ -147,16 +144,16 @@ async function main() { } if (!channelId) result.status = 'missing_id' - else if (channelsGroupedById.missing(channelId)) result.status = 'invalid_id' - else if (channelSearchRequestsBuffer.has(channelId)) result.status = 'duplicate' - else if (blocklistGroupedByChannelId.has(channelId)) result.status = 'blocked' - else if (streamsGroupedByChannelId.has(channelId)) result.status = 'fulfilled' + else if (channelsKeyById.missing(channelId)) result.status = 'invalid_id' + else if (channelSearchRequestsBuffer.has(streamId)) result.status = 'duplicate' + else if (blocklistRecordsGroupedByChannelId.has(channelId)) result.status = 'blocked' + else if (streamsGroupedById.has(streamId)) result.status = 'fulfilled' else { - const channelData = channelsGroupedById.get(channelId) + const channelData = channelsKeyById.get(channelId) if (channelData.length && channelData[0].closed) result.status = 'closed' } - channelSearchRequestsBuffer.set(channelId, true) + channelSearchRequestsBuffer.set(streamId, true) report.add(result) }) diff --git a/scripts/core/apiClient.ts b/scripts/core/apiClient.ts index 3b6291908..e4815a81a 100644 --- a/scripts/core/apiClient.ts +++ b/scripts/core/apiClient.ts @@ -1,59 +1,16 @@ -import { Logger, Storage } from '@freearhey/core' -import axios, { AxiosInstance, AxiosResponse, AxiosProgressEvent } from 'axios' -import cliProgress, { MultiBar } from 'cli-progress' -import numeral from 'numeral' +import axios, { AxiosInstance, AxiosResponse, AxiosRequestConfig } from 'axios' export class ApiClient { - progressBar: MultiBar - client: AxiosInstance - storage: Storage - logger: Logger + instance: AxiosInstance - constructor({ logger }: { logger: Logger }) { - this.logger = logger - this.client = axios.create({ + constructor() { + this.instance = axios.create({ + baseURL: 'https://iptv-org.github.io/api', responseType: 'stream' }) - this.storage = new Storage() - this.progressBar = new cliProgress.MultiBar({ - stopOnComplete: true, - hideCursor: true, - forceRedraw: true, - barsize: 36, - format(options, params, payload) { - const filename = payload.filename.padEnd(18, ' ') - const barsize = options.barsize || 40 - const percent = (params.progress * 100).toFixed(2) - const speed = payload.speed ? numeral(payload.speed).format('0.0 b') + '/s' : 'N/A' - const total = numeral(params.total).format('0.0 b') - const completeSize = Math.round(params.progress * barsize) - const incompleteSize = barsize - completeSize - const bar = - options.barCompleteString && options.barIncompleteString - ? options.barCompleteString.substr(0, completeSize) + - options.barGlue + - options.barIncompleteString.substr(0, incompleteSize) - : '-'.repeat(barsize) - - return `${filename} [${bar}] ${percent}% | ETA: ${params.eta}s | ${total} | ${speed}` - } - }) } - async download(filename: string) { - const stream = await this.storage.createStream(`temp/data/${filename}`) - - const bar = this.progressBar.create(0, 0, { filename }) - - this.client - .get(`https://iptv-org.github.io/api/${filename}`, { - onDownloadProgress({ total, loaded, rate }: AxiosProgressEvent) { - if (total) bar.setTotal(total) - bar.update(loaded, { speed: rate }) - } - }) - .then((response: AxiosResponse) => { - response.data.pipe(stream) - }) + get(url: string, options: AxiosRequestConfig): Promise { + return this.instance.get(url, options) } } diff --git a/scripts/core/dataLoader.ts b/scripts/core/dataLoader.ts new file mode 100644 index 000000000..2379edc9e --- /dev/null +++ b/scripts/core/dataLoader.ts @@ -0,0 +1,100 @@ +import { ApiClient } from './apiClient' +import { Storage } from '@freearhey/core' +import cliProgress, { MultiBar } from 'cli-progress' +import numeral from 'numeral' +import type { DataLoaderProps, DataLoaderData } from '../types/dataLoader' + +export class DataLoader { + client: ApiClient + storage: Storage + progressBar: MultiBar + + constructor(props: DataLoaderProps) { + this.client = new ApiClient() + this.storage = props.storage + this.progressBar = new cliProgress.MultiBar({ + stopOnComplete: true, + hideCursor: true, + forceRedraw: true, + barsize: 36, + format(options, params, payload) { + const filename = payload.filename.padEnd(18, ' ') + const barsize = options.barsize || 40 + const percent = (params.progress * 100).toFixed(2) + const speed = payload.speed ? numeral(payload.speed).format('0.0 b') + '/s' : 'N/A' + const total = numeral(params.total).format('0.0 b') + const completeSize = Math.round(params.progress * barsize) + const incompleteSize = barsize - completeSize + const bar = + options.barCompleteString && options.barIncompleteString + ? options.barCompleteString.substr(0, completeSize) + + options.barGlue + + options.barIncompleteString.substr(0, incompleteSize) + : '-'.repeat(barsize) + + return `${filename} [${bar}] ${percent}% | ETA: ${params.eta}s | ${total} | ${speed}` + } + }) + } + + async load(): Promise { + const [ + countries, + regions, + subdivisions, + languages, + categories, + blocklist, + channels, + feeds, + timezones, + guides, + streams + ] = await Promise.all([ + this.storage.json('countries.json'), + this.storage.json('regions.json'), + this.storage.json('subdivisions.json'), + this.storage.json('languages.json'), + this.storage.json('categories.json'), + this.storage.json('blocklist.json'), + this.storage.json('channels.json'), + this.storage.json('feeds.json'), + this.storage.json('timezones.json'), + this.storage.json('guides.json'), + this.storage.json('streams.json') + ]) + + return { + countries, + regions, + subdivisions, + languages, + categories, + blocklist, + channels, + feeds, + timezones, + guides, + streams + } + } + + async download(filename: string) { + if (!this.storage || !this.progressBar) return + + const stream = await this.storage.createStream(filename) + const progressBar = this.progressBar.create(0, 0, { filename }) + + this.client + .get(filename, { + responseType: 'stream', + onDownloadProgress({ total, loaded, rate }) { + if (total) progressBar.setTotal(total) + progressBar.update(loaded, { speed: rate }) + } + }) + .then(response => { + response.data.pipe(stream) + }) + } +} diff --git a/scripts/core/dataProcessor.ts b/scripts/core/dataProcessor.ts new file mode 100644 index 000000000..3290fe5b3 --- /dev/null +++ b/scripts/core/dataProcessor.ts @@ -0,0 +1,110 @@ +import { DataLoaderData } from '../types/dataLoader' +import { Collection } from '@freearhey/core' +import { + BlocklistRecord, + Subdivision, + Category, + Language, + Timezone, + Channel, + Country, + Region, + Stream, + Guide, + Feed +} from '../models' + +export class DataProcessor { + constructor() {} + + process(data: DataLoaderData) { + const categories = new Collection(data.categories).map(data => new Category(data)) + const categoriesKeyById = categories.keyBy((category: Category) => category.id) + + const subdivisions = new Collection(data.subdivisions).map(data => new Subdivision(data)) + const subdivisionsKeyByCode = subdivisions.keyBy((subdivision: Subdivision) => subdivision.code) + const subdivisionsGroupedByCountryCode = subdivisions.groupBy( + (subdivision: Subdivision) => subdivision.countryCode + ) + + let regions = new Collection(data.regions).map(data => new Region(data)) + const regionsKeyByCode = regions.keyBy((region: Region) => region.code) + + const blocklistRecords = new Collection(data.blocklist).map(data => new BlocklistRecord(data)) + const blocklistRecordsGroupedByChannelId = blocklistRecords.groupBy( + (blocklistRecord: BlocklistRecord) => blocklistRecord.channelId + ) + + const streams = new Collection(data.streams).map(data => new Stream(data)) + const streamsGroupedById = streams.groupBy((stream: Stream) => stream.getId()) + + const guides = new Collection(data.guides).map(data => new Guide(data)) + const guidesGroupedByStreamId = guides.groupBy((guide: Guide) => guide.getStreamId()) + + const languages = new Collection(data.languages).map(data => new Language(data)) + const languagesKeyByCode = languages.keyBy((language: Language) => language.code) + + const countries = new Collection(data.countries).map(data => + new Country(data) + .withRegions(regions) + .withLanguage(languagesKeyByCode) + .withSubdivisions(subdivisionsGroupedByCountryCode) + ) + const countriesKeyByCode = countries.keyBy((country: Country) => country.code) + + regions = regions.map((region: Region) => region.withCountries(countriesKeyByCode)) + + const timezones = new Collection(data.timezones).map(data => + new Timezone(data).withCountries(countriesKeyByCode) + ) + const timezonesKeyById = timezones.keyBy((timezone: Timezone) => timezone.id) + + let channels = new Collection(data.channels).map(data => + new Channel(data) + .withCategories(categoriesKeyById) + .withCountry(countriesKeyByCode) + .withSubdivision(subdivisionsKeyByCode) + .withCategories(categoriesKeyById) + ) + const channelsKeyById = channels.keyBy((channel: Channel) => channel.id) + + let feeds = new Collection(data.feeds).map(data => + new Feed(data) + .withChannel(channelsKeyById) + .withLanguages(languagesKeyByCode) + .withTimezones(timezonesKeyById) + .withBroadcastCountries(countriesKeyByCode, regionsKeyByCode, subdivisionsKeyByCode) + .withBroadcastRegions(regions) + .withBroadcastSubdivisions(subdivisionsKeyByCode) + ) + const feedsGroupedByChannelId = feeds.groupBy((feed: Feed) => feed.channelId) + + channels = channels.map((channel: Channel) => channel.withFeeds(feedsGroupedByChannelId)) + + return { + blocklistRecordsGroupedByChannelId, + subdivisionsGroupedByCountryCode, + feedsGroupedByChannelId, + guidesGroupedByStreamId, + subdivisionsKeyByCode, + countriesKeyByCode, + languagesKeyByCode, + streamsGroupedById, + categoriesKeyById, + timezonesKeyById, + regionsKeyByCode, + blocklistRecords, + channelsKeyById, + subdivisions, + categories, + countries, + languages, + timezones, + channels, + regions, + streams, + guides, + feeds + } + } +} diff --git a/scripts/core/htmlTable.ts b/scripts/core/htmlTable.ts index 1caa85fa0..b4f7b42ac 100644 --- a/scripts/core/htmlTable.ts +++ b/scripts/core/htmlTable.ts @@ -16,15 +16,15 @@ export class HTMLTable { } toString() { - let output = '\n' + let output = '
\r\n' - output += ' \n ' + output += ' \r\n ' for (const column of this.columns) { output += `` } - output += '\n \n' + output += '\r\n \r\n' - output += ' \n' + output += ' \r\n' for (const item of this.data) { output += ' ' let i = 0 @@ -35,9 +35,9 @@ export class HTMLTable { output += `${item[prop]}` i++ } - output += '\n' + output += '\r\n' } - output += ' \n' + output += ' \r\n' output += '
${column.name}
' diff --git a/scripts/core/index.ts b/scripts/core/index.ts index cdac07f58..d32237310 100644 --- a/scripts/core/index.ts +++ b/scripts/core/index.ts @@ -1,11 +1,13 @@ -export * from './playlistParser' -export * from './numberParser' -export * from './logParser' -export * from './markdown' +export * from './apiClient' +export * from './cliTable' +export * from './dataProcessor' +export * from './dataLoader' +export * from './htmlTable' +export * from './issueData' export * from './issueLoader' export * from './issueParser' -export * from './htmlTable' -export * from './apiClient' -export * from './issueData' +export * from './logParser' +export * from './markdown' +export * from './numberParser' +export * from './playlistParser' export * from './streamTester' -export * from './cliTable' diff --git a/scripts/core/playlistParser.ts b/scripts/core/playlistParser.ts index b28876663..7d388dd20 100644 --- a/scripts/core/playlistParser.ts +++ b/scripts/core/playlistParser.ts @@ -5,18 +5,18 @@ import { Stream } from '../models' type PlaylistPareserProps = { storage: Storage feedsGroupedByChannelId: Dictionary - channelsGroupedById: Dictionary + channelsKeyById: Dictionary } export class PlaylistParser { storage: Storage feedsGroupedByChannelId: Dictionary - channelsGroupedById: Dictionary + channelsKeyById: Dictionary - constructor({ storage, feedsGroupedByChannelId, channelsGroupedById }: PlaylistPareserProps) { + constructor({ storage, feedsGroupedByChannelId, channelsKeyById }: PlaylistPareserProps) { this.storage = storage this.feedsGroupedByChannelId = feedsGroupedByChannelId - this.channelsGroupedById = channelsGroupedById + this.channelsKeyById = channelsKeyById } async parse(files: string[]): Promise { @@ -35,9 +35,10 @@ export class PlaylistParser { const parsed: parser.Playlist = parser.parse(content) const streams = new Collection(parsed.items).map((data: parser.PlaylistItem) => { - const stream = new Stream(data) + const stream = new Stream() + .fromPlaylistItem(data) .withFeed(this.feedsGroupedByChannelId) - .withChannel(this.channelsGroupedById) + .withChannel(this.channelsKeyById) .setFilepath(filepath) return stream diff --git a/scripts/core/streamTester.ts b/scripts/core/streamTester.ts index 89c44de74..860844b40 100644 --- a/scripts/core/streamTester.ts +++ b/scripts/core/streamTester.ts @@ -18,8 +18,8 @@ export class StreamTester { return this.checker.checkStream({ url: stream.url, http: { - referrer: stream.getHttpReferrer(), - 'user-agent': stream.getHttpUserAgent() + referrer: stream.getReferrer(), + 'user-agent': stream.getUserAgent() } }) } diff --git a/scripts/generators/categoriesGenerator.ts b/scripts/generators/categoriesGenerator.ts index cd20b6ea4..f6d455d4b 100644 --- a/scripts/generators/categoriesGenerator.ts +++ b/scripts/generators/categoriesGenerator.ts @@ -1,25 +1,26 @@ -import { Generator } from './generator' -import { Collection, Storage, Logger } from '@freearhey/core' +import { Collection, Storage, Logger, File } from '@freearhey/core' import { Stream, Category, Playlist } from '../models' import { PUBLIC_DIR } from '../constants' +import { Generator } from './generator' +import { EOL } from 'node:os' type CategoriesGeneratorProps = { streams: Collection categories: Collection - logger: Logger + logFile: File } export class CategoriesGenerator implements Generator { streams: Collection categories: Collection storage: Storage - logger: Logger + logFile: File - constructor({ streams, categories, logger }: CategoriesGeneratorProps) { + constructor({ streams, categories, logFile }: CategoriesGeneratorProps) { this.streams = streams this.categories = categories this.storage = new Storage(PUBLIC_DIR) - this.logger = logger + this.logFile = logFile } async generate() { @@ -37,8 +38,8 @@ export class CategoriesGenerator implements Generator { const playlist = new Playlist(categoryStreams, { public: true }) const filepath = `categories/${category.id}.m3u` await this.storage.save(filepath, playlist.toString()) - this.logger.info( - JSON.stringify({ type: 'category', filepath, count: playlist.streams.count() }) + this.logFile.append( + JSON.stringify({ type: 'category', filepath, count: playlist.streams.count() }) + EOL ) }) @@ -46,8 +47,8 @@ export class CategoriesGenerator implements Generator { const playlist = new Playlist(undefinedStreams, { public: true }) const filepath = 'categories/undefined.m3u' await this.storage.save(filepath, playlist.toString()) - this.logger.info( - JSON.stringify({ type: 'category', filepath, count: playlist.streams.count() }) + this.logFile.append( + JSON.stringify({ type: 'category', filepath, count: playlist.streams.count() }) + EOL ) } } diff --git a/scripts/generators/countriesGenerator.ts b/scripts/generators/countriesGenerator.ts index c935da5a4..7dc707cf9 100644 --- a/scripts/generators/countriesGenerator.ts +++ b/scripts/generators/countriesGenerator.ts @@ -1,25 +1,26 @@ -import { Generator } from './generator' -import { Collection, Storage, Logger } from '@freearhey/core' import { Country, Subdivision, Stream, Playlist } from '../models' +import { Collection, Storage, File } from '@freearhey/core' import { PUBLIC_DIR } from '../constants' +import { Generator } from './generator' +import { EOL } from 'node:os' type CountriesGeneratorProps = { streams: Collection countries: Collection - logger: Logger + logFile: File } export class CountriesGenerator implements Generator { streams: Collection countries: Collection storage: Storage - logger: Logger + logFile: File - constructor({ streams, countries, logger }: CountriesGeneratorProps) { + constructor({ streams, countries, logFile }: CountriesGeneratorProps) { this.streams = streams this.countries = countries this.storage = new Storage(PUBLIC_DIR) - this.logger = logger + this.logFile = logFile } async generate(): Promise { @@ -36,8 +37,8 @@ export class CountriesGenerator implements Generator { const playlist = new Playlist(countryStreams, { public: true }) const filepath = `countries/${country.code.toLowerCase()}.m3u` await this.storage.save(filepath, playlist.toString()) - this.logger.info( - JSON.stringify({ type: 'country', filepath, count: playlist.streams.count() }) + this.logFile.append( + JSON.stringify({ type: 'country', filepath, count: playlist.streams.count() }) + EOL ) country.getSubdivisions().forEach(async (subdivision: Subdivision) => { @@ -50,8 +51,8 @@ export class CountriesGenerator implements Generator { const playlist = new Playlist(subdivisionStreams, { public: true }) const filepath = `subdivisions/${subdivision.code.toLowerCase()}.m3u` await this.storage.save(filepath, playlist.toString()) - this.logger.info( - JSON.stringify({ type: 'subdivision', filepath, count: playlist.streams.count() }) + this.logFile.append( + JSON.stringify({ type: 'subdivision', filepath, count: playlist.streams.count() }) + EOL ) }) }) @@ -60,12 +61,12 @@ export class CountriesGenerator implements Generator { const undefinedPlaylist = new Playlist(undefinedStreams, { public: true }) const undefinedFilepath = 'countries/undefined.m3u' await this.storage.save(undefinedFilepath, undefinedPlaylist.toString()) - this.logger.info( + this.logFile.append( JSON.stringify({ type: 'country', filepath: undefinedFilepath, count: undefinedPlaylist.streams.count() - }) + }) + EOL ) } } diff --git a/scripts/generators/indexCategoryGenerator.ts b/scripts/generators/indexCategoryGenerator.ts index 529ee8336..665f4cb0c 100644 --- a/scripts/generators/indexCategoryGenerator.ts +++ b/scripts/generators/indexCategoryGenerator.ts @@ -1,22 +1,23 @@ -import { Generator } from './generator' -import { Collection, Storage, Logger } from '@freearhey/core' +import { Collection, Storage, File } from '@freearhey/core' import { Stream, Playlist, Category } from '../models' import { PUBLIC_DIR } from '../constants' +import { Generator } from './generator' +import { EOL } from 'node:os' type IndexCategoryGeneratorProps = { streams: Collection - logger: Logger + logFile: File } export class IndexCategoryGenerator implements Generator { streams: Collection storage: Storage - logger: Logger + logFile: File - constructor({ streams, logger }: IndexCategoryGeneratorProps) { + constructor({ streams, logFile }: IndexCategoryGeneratorProps) { this.streams = streams this.storage = new Storage(PUBLIC_DIR) - this.logger = logger + this.logFile = logFile } async generate(): Promise { @@ -48,6 +49,8 @@ export class IndexCategoryGenerator implements Generator { const playlist = new Playlist(groupedStreams, { public: true }) const filepath = 'index.category.m3u' await this.storage.save(filepath, playlist.toString()) - this.logger.info(JSON.stringify({ type: 'index', filepath, count: playlist.streams.count() })) + this.logFile.append( + JSON.stringify({ type: 'index', filepath, count: playlist.streams.count() }) + EOL + ) } } diff --git a/scripts/generators/indexCountryGenerator.ts b/scripts/generators/indexCountryGenerator.ts index c65a43734..82eb335ef 100644 --- a/scripts/generators/indexCountryGenerator.ts +++ b/scripts/generators/indexCountryGenerator.ts @@ -1,22 +1,23 @@ -import { Generator } from './generator' -import { Collection, Storage, Logger } from '@freearhey/core' +import { Collection, Storage, File } from '@freearhey/core' import { Stream, Playlist, Country } from '../models' import { PUBLIC_DIR } from '../constants' +import { Generator } from './generator' +import { EOL } from 'node:os' type IndexCountryGeneratorProps = { streams: Collection - logger: Logger + logFile: File } export class IndexCountryGenerator implements Generator { streams: Collection storage: Storage - logger: Logger + logFile: File - constructor({ streams, logger }: IndexCountryGeneratorProps) { + constructor({ streams, logFile }: IndexCountryGeneratorProps) { this.streams = streams this.storage = new Storage(PUBLIC_DIR) - this.logger = logger + this.logFile = logFile } async generate(): Promise { @@ -56,6 +57,8 @@ export class IndexCountryGenerator implements Generator { const playlist = new Playlist(groupedStreams, { public: true }) const filepath = 'index.country.m3u' await this.storage.save(filepath, playlist.toString()) - this.logger.info(JSON.stringify({ type: 'index', filepath, count: playlist.streams.count() })) + this.logFile.append( + JSON.stringify({ type: 'index', filepath, count: playlist.streams.count() }) + EOL + ) } } diff --git a/scripts/generators/indexGenerator.ts b/scripts/generators/indexGenerator.ts index fafda061f..5cfa86c66 100644 --- a/scripts/generators/indexGenerator.ts +++ b/scripts/generators/indexGenerator.ts @@ -1,22 +1,23 @@ -import { Collection, Logger, Storage } from '@freearhey/core' +import { Collection, File, Storage } from '@freearhey/core' import { Stream, Playlist } from '../models' -import { Generator } from './generator' import { PUBLIC_DIR } from '../constants' +import { Generator } from './generator' +import { EOL } from 'node:os' type IndexGeneratorProps = { streams: Collection - logger: Logger + logFile: File } export class IndexGenerator implements Generator { streams: Collection storage: Storage - logger: Logger + logFile: File - constructor({ streams, logger }: IndexGeneratorProps) { + constructor({ streams, logFile }: IndexGeneratorProps) { this.streams = streams this.storage = new Storage(PUBLIC_DIR) - this.logger = logger + this.logFile = logFile } async generate(): Promise { @@ -27,6 +28,8 @@ export class IndexGenerator implements Generator { const playlist = new Playlist(sfwStreams, { public: true }) const filepath = 'index.m3u' await this.storage.save(filepath, playlist.toString()) - this.logger.info(JSON.stringify({ type: 'index', filepath, count: playlist.streams.count() })) + this.logFile.append( + JSON.stringify({ type: 'index', filepath, count: playlist.streams.count() }) + EOL + ) } } diff --git a/scripts/generators/indexLanguageGenerator.ts b/scripts/generators/indexLanguageGenerator.ts index 1116eb740..3df9f71f2 100644 --- a/scripts/generators/indexLanguageGenerator.ts +++ b/scripts/generators/indexLanguageGenerator.ts @@ -1,22 +1,23 @@ -import { Generator } from './generator' -import { Collection, Storage, Logger } from '@freearhey/core' +import { Collection, Storage, File } from '@freearhey/core' import { Stream, Playlist, Language } from '../models' import { PUBLIC_DIR } from '../constants' +import { Generator } from './generator' +import { EOL } from 'node:os' type IndexLanguageGeneratorProps = { streams: Collection - logger: Logger + logFile: File } export class IndexLanguageGenerator implements Generator { streams: Collection storage: Storage - logger: Logger + logFile: File - constructor({ streams, logger }: IndexLanguageGeneratorProps) { + constructor({ streams, logFile }: IndexLanguageGeneratorProps) { this.streams = streams this.storage = new Storage(PUBLIC_DIR) - this.logger = logger + this.logFile = logFile } async generate(): Promise { @@ -47,6 +48,8 @@ export class IndexLanguageGenerator implements Generator { const playlist = new Playlist(groupedStreams, { public: true }) const filepath = 'index.language.m3u' await this.storage.save(filepath, playlist.toString()) - this.logger.info(JSON.stringify({ type: 'index', filepath, count: playlist.streams.count() })) + this.logFile.append( + JSON.stringify({ type: 'index', filepath, count: playlist.streams.count() }) + EOL + ) } } diff --git a/scripts/generators/indexNsfwGenerator.ts b/scripts/generators/indexNsfwGenerator.ts index a89cf0a10..e1e98375b 100644 --- a/scripts/generators/indexNsfwGenerator.ts +++ b/scripts/generators/indexNsfwGenerator.ts @@ -1,22 +1,23 @@ -import { Collection, Logger, Storage } from '@freearhey/core' +import { Collection, File, Storage } from '@freearhey/core' import { Stream, Playlist } from '../models' -import { Generator } from './generator' import { PUBLIC_DIR } from '../constants' +import { Generator } from './generator' +import { EOL } from 'node:os' type IndexNsfwGeneratorProps = { streams: Collection - logger: Logger + logFile: File } export class IndexNsfwGenerator implements Generator { streams: Collection storage: Storage - logger: Logger + logFile: File - constructor({ streams, logger }: IndexNsfwGeneratorProps) { + constructor({ streams, logFile }: IndexNsfwGeneratorProps) { this.streams = streams this.storage = new Storage(PUBLIC_DIR) - this.logger = logger + this.logFile = logFile } async generate(): Promise { @@ -25,6 +26,8 @@ export class IndexNsfwGenerator implements Generator { const playlist = new Playlist(allStreams, { public: true }) const filepath = 'index.nsfw.m3u' await this.storage.save(filepath, playlist.toString()) - this.logger.info(JSON.stringify({ type: 'index', filepath, count: playlist.streams.count() })) + this.logFile.append( + JSON.stringify({ type: 'index', filepath, count: playlist.streams.count() }) + EOL + ) } } diff --git a/scripts/generators/indexRegionGenerator.ts b/scripts/generators/indexRegionGenerator.ts index 94537c9af..c462fcfce 100644 --- a/scripts/generators/indexRegionGenerator.ts +++ b/scripts/generators/indexRegionGenerator.ts @@ -1,25 +1,26 @@ -import { Generator } from './generator' -import { Collection, Storage, Logger } from '@freearhey/core' +import { Collection, Storage, File } from '@freearhey/core' import { Stream, Playlist, Region } from '../models' import { PUBLIC_DIR } from '../constants' +import { Generator } from './generator' +import { EOL } from 'node:os' type IndexRegionGeneratorProps = { streams: Collection regions: Collection - logger: Logger + logFile: File } export class IndexRegionGenerator implements Generator { streams: Collection regions: Collection storage: Storage - logger: Logger + logFile: File - constructor({ streams, regions, logger }: IndexRegionGeneratorProps) { + constructor({ streams, regions, logFile }: IndexRegionGeneratorProps) { this.streams = streams this.regions = regions this.storage = new Storage(PUBLIC_DIR) - this.logger = logger + this.logFile = logFile } async generate(): Promise { @@ -58,6 +59,8 @@ export class IndexRegionGenerator implements Generator { const playlist = new Playlist(groupedStreams, { public: true }) const filepath = 'index.region.m3u' await this.storage.save(filepath, playlist.toString()) - this.logger.info(JSON.stringify({ type: 'index', filepath, count: playlist.streams.count() })) + this.logFile.append( + JSON.stringify({ type: 'index', filepath, count: playlist.streams.count() }) + EOL + ) } } diff --git a/scripts/generators/languagesGenerator.ts b/scripts/generators/languagesGenerator.ts index 114fcddb2..f7ae9976e 100644 --- a/scripts/generators/languagesGenerator.ts +++ b/scripts/generators/languagesGenerator.ts @@ -1,19 +1,20 @@ -import { Generator } from './generator' -import { Collection, Storage, Logger } from '@freearhey/core' +import { Collection, Storage, File } from '@freearhey/core' import { Playlist, Language, Stream } from '../models' import { PUBLIC_DIR } from '../constants' +import { Generator } from './generator' +import { EOL } from 'node:os' -type LanguagesGeneratorProps = { streams: Collection; logger: Logger } +type LanguagesGeneratorProps = { streams: Collection; logFile: File } export class LanguagesGenerator implements Generator { streams: Collection storage: Storage - logger: Logger + logFile: File - constructor({ streams, logger }: LanguagesGeneratorProps) { + constructor({ streams, logFile }: LanguagesGeneratorProps) { this.streams = streams this.storage = new Storage(PUBLIC_DIR) - this.logger = logger + this.logFile = logFile } async generate(): Promise { @@ -38,8 +39,8 @@ export class LanguagesGenerator implements Generator { const playlist = new Playlist(languageStreams, { public: true }) const filepath = `languages/${language.code}.m3u` await this.storage.save(filepath, playlist.toString()) - this.logger.info( - JSON.stringify({ type: 'language', filepath, count: playlist.streams.count() }) + this.logFile.append( + JSON.stringify({ type: 'language', filepath, count: playlist.streams.count() }) + EOL ) }) @@ -50,8 +51,8 @@ export class LanguagesGenerator implements Generator { const playlist = new Playlist(undefinedStreams, { public: true }) const filepath = 'languages/undefined.m3u' await this.storage.save(filepath, playlist.toString()) - this.logger.info( - JSON.stringify({ type: 'language', filepath, count: playlist.streams.count() }) + this.logFile.append( + JSON.stringify({ type: 'language', filepath, count: playlist.streams.count() }) + EOL ) } } diff --git a/scripts/generators/regionsGenerator.ts b/scripts/generators/regionsGenerator.ts index fb0a5d688..4d649a351 100644 --- a/scripts/generators/regionsGenerator.ts +++ b/scripts/generators/regionsGenerator.ts @@ -1,25 +1,26 @@ -import { Generator } from './generator' -import { Collection, Storage, Logger } from '@freearhey/core' +import { Collection, Storage, File } from '@freearhey/core' import { Playlist, Region, Stream } from '../models' import { PUBLIC_DIR } from '../constants' +import { Generator } from './generator' +import { EOL } from 'node:os' type RegionsGeneratorProps = { streams: Collection regions: Collection - logger: Logger + logFile: File } export class RegionsGenerator implements Generator { streams: Collection regions: Collection storage: Storage - logger: Logger + logFile: File - constructor({ streams, regions, logger }: RegionsGeneratorProps) { + constructor({ streams, regions, logFile }: RegionsGeneratorProps) { this.streams = streams this.regions = regions this.storage = new Storage(PUBLIC_DIR) - this.logger = logger + this.logFile = logFile } async generate(): Promise { @@ -35,8 +36,8 @@ export class RegionsGenerator implements Generator { const playlist = new Playlist(regionStreams, { public: true }) const filepath = `regions/${region.code.toLowerCase()}.m3u` await this.storage.save(filepath, playlist.toString()) - this.logger.info( - JSON.stringify({ type: 'region', filepath, count: playlist.streams.count() }) + this.logFile.append( + JSON.stringify({ type: 'region', filepath, count: playlist.streams.count() }) + EOL ) }) @@ -44,18 +45,20 @@ export class RegionsGenerator implements Generator { const internationalPlaylist = new Playlist(internationalStreams, { public: true }) const internationalFilepath = 'regions/int.m3u' await this.storage.save(internationalFilepath, internationalPlaylist.toString()) - this.logger.info( + this.logFile.append( JSON.stringify({ type: 'region', filepath: internationalFilepath, count: internationalPlaylist.streams.count() - }) + }) + EOL ) const undefinedStreams = streams.filter((stream: Stream) => !stream.hasBroadcastArea()) const playlist = new Playlist(undefinedStreams, { public: true }) const filepath = 'regions/undefined.m3u' await this.storage.save(filepath, playlist.toString()) - this.logger.info(JSON.stringify({ type: 'region', filepath, count: playlist.streams.count() })) + this.logFile.append( + JSON.stringify({ type: 'region', filepath, count: playlist.streams.count() }) + EOL + ) } } diff --git a/scripts/models/blocked.ts b/scripts/models/blocked.ts deleted file mode 100644 index 29041278b..000000000 --- a/scripts/models/blocked.ts +++ /dev/null @@ -1,17 +0,0 @@ -type BlockedProps = { - channel: string - reason: string - ref: string -} - -export class Blocked { - channelId: string - reason: string - ref: string - - constructor(data: BlockedProps) { - this.channelId = data.channel - this.reason = data.reason - this.ref = data.ref - } -} diff --git a/scripts/models/blocklistRecord.ts b/scripts/models/blocklistRecord.ts new file mode 100644 index 000000000..632a1d4dd --- /dev/null +++ b/scripts/models/blocklistRecord.ts @@ -0,0 +1,15 @@ +import type { BlocklistRecordData } from '../types/blocklistRecord' + +export class BlocklistRecord { + channelId: string + reason: string + ref: string + + constructor(data?: BlocklistRecordData) { + if (!data) return + + this.channelId = data.channel + this.reason = data.reason + this.ref = data.ref + } +} diff --git a/scripts/models/category.ts b/scripts/models/category.ts index 17ff9af12..5b228a86d 100644 --- a/scripts/models/category.ts +++ b/scripts/models/category.ts @@ -1,7 +1,4 @@ -type CategoryData = { - id: string - name: string -} +import type { CategoryData, CategorySerializedData } from '../types/category' export class Category { id: string @@ -11,4 +8,11 @@ export class Category { this.id = data.id this.name = data.name } + + serialize(): CategorySerializedData { + return { + id: this.id, + name: this.name + } + } } diff --git a/scripts/models/channel.ts b/scripts/models/channel.ts index 1d4c5cf8d..cdc09af0a 100644 --- a/scripts/models/channel.ts +++ b/scripts/models/channel.ts @@ -1,23 +1,6 @@ import { Collection, Dictionary } from '@freearhey/core' -import { Category, Country, Subdivision } from './index' - -type ChannelData = { - id: string - name: string - alt_names: string[] - network: string - owners: Collection - country: string - subdivision: string - city: string - categories: Collection - is_nsfw: boolean - launched: string - closed: string - replaced_by: string - website: string - logo: string -} +import { Category, Country, Feed, Guide, Stream, Subdivision } from './index' +import type { ChannelData, ChannelSearchableData, ChannelSerializedData } from '../types/channel' export class Channel { id: string @@ -31,15 +14,18 @@ export class Channel { subdivision?: Subdivision cityName?: string categoryIds: Collection - categories?: Collection + categories: Collection = new Collection() isNSFW: boolean launched?: string closed?: string replacedBy?: string website?: string logo: string + feeds?: Collection + + constructor(data?: ChannelData) { + if (!data) return - constructor(data: ChannelData) { this.id = data.id this.name = data.name this.altNames = new Collection(data.alt_names) @@ -57,28 +43,34 @@ export class Channel { this.logo = data.logo } - withSubdivision(subdivisionsGroupedByCode: Dictionary): this { + withSubdivision(subdivisionsKeyByCode: Dictionary): this { if (!this.subdivisionCode) return this - this.subdivision = subdivisionsGroupedByCode.get(this.subdivisionCode) + this.subdivision = subdivisionsKeyByCode.get(this.subdivisionCode) return this } - withCountry(countriesGroupedByCode: Dictionary): this { - this.country = countriesGroupedByCode.get(this.countryCode) + withCountry(countriesKeyByCode: Dictionary): this { + this.country = countriesKeyByCode.get(this.countryCode) return this } - withCategories(groupedCategories: Dictionary): this { + withCategories(categoriesKeyById: Dictionary): this { this.categories = this.categoryIds - .map((id: string) => groupedCategories.get(id)) + .map((id: string) => categoriesKeyById.get(id)) .filter(Boolean) return this } + withFeeds(feedsGroupedByChannelId: Dictionary): this { + this.feeds = new Collection(feedsGroupedByChannelId.get(this.id)) + + return this + } + getCountry(): Country | undefined { return this.country } @@ -102,7 +94,106 @@ export class Channel { ) } + getFeeds(): Collection { + if (!this.feeds) return new Collection() + + return this.feeds + } + + getGuides(): Collection { + let guides = new Collection() + + this.getFeeds().forEach((feed: Feed) => { + guides = guides.concat(feed.getGuides()) + }) + + return guides + } + + getGuideNames(): Collection { + return this.getGuides() + .map((guide: Guide) => guide.siteName) + .uniq() + } + + getStreams(): Collection { + let streams = new Collection() + + this.getFeeds().forEach((feed: Feed) => { + streams = streams.concat(feed.getStreams()) + }) + + return streams + } + + getStreamNames(): Collection { + return this.getStreams() + .map((stream: Stream) => stream.getName()) + .uniq() + } + + getFeedFullNames(): Collection { + return this.getFeeds() + .map((feed: Feed) => feed.getFullName()) + .uniq() + } + isSFW(): boolean { return this.isNSFW === false } + + getSearchable(): ChannelSearchableData { + return { + id: this.id, + name: this.name, + altNames: this.altNames.all(), + guideNames: this.getGuideNames().all(), + streamNames: this.getStreamNames().all(), + feedFullNames: this.getFeedFullNames().all() + } + } + + serialize(): ChannelSerializedData { + return { + id: this.id, + name: this.name, + altNames: this.altNames.all(), + network: this.network, + owners: this.owners.all(), + countryCode: this.countryCode, + country: this.country ? this.country.serialize() : undefined, + subdivisionCode: this.subdivisionCode, + subdivision: this.subdivision ? this.subdivision.serialize() : undefined, + cityName: this.cityName, + categoryIds: this.categoryIds.all(), + categories: this.categories.map((category: Category) => category.serialize()).all(), + isNSFW: this.isNSFW, + launched: this.launched, + closed: this.closed, + replacedBy: this.replacedBy, + website: this.website, + logo: this.logo + } + } + + deserialize(data: ChannelSerializedData): this { + this.id = data.id + this.name = data.name + this.altNames = new Collection(data.altNames) + this.network = data.network + this.owners = new Collection(data.owners) + this.countryCode = data.countryCode + this.country = data.country ? new Country().deserialize(data.country) : undefined + this.subdivisionCode = data.subdivisionCode + this.cityName = data.cityName + this.categoryIds = new Collection(data.categoryIds) + this.isNSFW = data.isNSFW + this.launched = data.launched + this.closed = data.closed + this.replacedBy = data.replacedBy + this.website = data.website + this.logo = data.logo + + return this + } } diff --git a/scripts/models/country.ts b/scripts/models/country.ts index ac822a235..780c4413f 100644 --- a/scripts/models/country.ts +++ b/scripts/models/country.ts @@ -1,12 +1,8 @@ import { Collection, Dictionary } from '@freearhey/core' -import { Region, Language } from '.' - -type CountryData = { - code: string - name: string - lang: string - flag: string -} +import { Region, Language, Subdivision } from '.' +import type { CountryData, CountrySerializedData } from '../types/country' +import { SubdivisionSerializedData } from '../types/subdivision' +import { RegionSerializedData } from '../types/region' export class Country { code: string @@ -17,7 +13,9 @@ export class Country { subdivisions?: Collection regions?: Collection - constructor(data: CountryData) { + constructor(data?: CountryData) { + if (!data) return + this.code = data.code this.name = data.name this.flag = data.flag @@ -38,8 +36,8 @@ export class Country { return this } - withLanguage(languagesGroupedByCode: Dictionary): this { - this.language = languagesGroupedByCode.get(this.languageCode) + withLanguage(languagesKeyByCode: Dictionary): this { + this.language = languagesKeyByCode.get(this.languageCode) return this } @@ -55,4 +53,34 @@ export class Country { getSubdivisions(): Collection { return this.subdivisions || new Collection() } + + serialize(): CountrySerializedData { + return { + code: this.code, + name: this.name, + flag: this.flag, + languageCode: this.languageCode, + language: this.language ? this.language.serialize() : null, + subdivisions: this.subdivisions + ? this.subdivisions.map((subdivision: Subdivision) => subdivision.serialize()).all() + : [], + regions: this.regions ? this.regions.map((region: Region) => region.serialize()).all() : [] + } + } + + deserialize(data: CountrySerializedData): this { + this.code = data.code + this.name = data.name + this.flag = data.flag + this.languageCode = data.languageCode + this.language = data.language ? new Language().deserialize(data.language) : undefined + this.subdivisions = new Collection(data.subdivisions).map((data: SubdivisionSerializedData) => + new Subdivision().deserialize(data) + ) + this.regions = new Collection(data.regions).map((data: RegionSerializedData) => + new Region().deserialize(data) + ) + + return this + } } diff --git a/scripts/models/feed.ts b/scripts/models/feed.ts index 03e34762b..2b1fa9d34 100644 --- a/scripts/models/feed.ts +++ b/scripts/models/feed.ts @@ -1,16 +1,6 @@ import { Collection, Dictionary } from '@freearhey/core' import { Country, Language, Region, Channel, Subdivision } from './index' - -type FeedData = { - channel: string - id: string - name: string - is_main: boolean - broadcast_area: Collection - languages: Collection - timezones: Collection - video_format: string -} +import type { FeedData } from '../types/feed' export class Feed { channelId: string @@ -30,6 +20,8 @@ export class Feed { timezoneIds: Collection timezones?: Collection videoFormat: string + guides?: Collection + streams?: Collection constructor(data: FeedData) { this.channelId = data.channel @@ -61,40 +53,58 @@ export class Feed { }) } - withChannel(channelsGroupedById: Dictionary): this { - this.channel = channelsGroupedById.get(this.channelId) + withChannel(channelsKeyById: Dictionary): this { + this.channel = channelsKeyById.get(this.channelId) return this } - withLanguages(languagesGroupedByCode: Dictionary): this { + withStreams(streamsGroupedById: Dictionary): this { + this.streams = new Collection(streamsGroupedById.get(`${this.channelId}@${this.id}`)) + + if (this.isMain) { + this.streams = this.streams.concat(new Collection(streamsGroupedById.get(this.channelId))) + } + + return this + } + + withGuides(guidesGroupedByStreamId: Dictionary): this { + this.guides = new Collection(guidesGroupedByStreamId.get(`${this.channelId}@${this.id}`)) + + if (this.isMain) { + this.guides = this.guides.concat(new Collection(guidesGroupedByStreamId.get(this.channelId))) + } + + return this + } + + withLanguages(languagesKeyByCode: Dictionary): this { this.languages = this.languageCodes - .map((code: string) => languagesGroupedByCode.get(code)) + .map((code: string) => languagesKeyByCode.get(code)) .filter(Boolean) return this } - withTimezones(timezonesGroupedById: Dictionary): this { - this.timezones = this.timezoneIds - .map((id: string) => timezonesGroupedById.get(id)) - .filter(Boolean) + withTimezones(timezonesKeyById: Dictionary): this { + this.timezones = this.timezoneIds.map((id: string) => timezonesKeyById.get(id)).filter(Boolean) return this } - withBroadcastSubdivisions(subdivisionsGroupedByCode: Dictionary): this { + withBroadcastSubdivisions(subdivisionsKeyByCode: Dictionary): this { this.broadcastSubdivisions = this.broadcastSubdivisionCodes.map((code: string) => - subdivisionsGroupedByCode.get(code) + subdivisionsKeyByCode.get(code) ) return this } withBroadcastCountries( - countriesGroupedByCode: Dictionary, - regionsGroupedByCode: Dictionary, - subdivisionsGroupedByCode: Dictionary + countriesKeyByCode: Dictionary, + regionsKeyByCode: Dictionary, + subdivisionsKeyByCode: Dictionary ): this { let broadcastCountries = new Collection() @@ -104,22 +114,22 @@ export class Feed { } this.broadcastCountryCodes.forEach((code: string) => { - broadcastCountries.add(countriesGroupedByCode.get(code)) + broadcastCountries.add(countriesKeyByCode.get(code)) }) this.broadcastRegionCodes.forEach((code: string) => { - const region: Region = regionsGroupedByCode.get(code) + const region: Region = regionsKeyByCode.get(code) if (region) { region.countryCodes.forEach((countryCode: string) => { - broadcastCountries.add(countriesGroupedByCode.get(countryCode)) + broadcastCountries.add(countriesKeyByCode.get(countryCode)) }) } }) this.broadcastSubdivisionCodes.forEach((code: string) => { - const subdivision: Subdivision = subdivisionsGroupedByCode.get(code) + const subdivision: Subdivision = subdivisionsKeyByCode.get(code) if (subdivision) { - broadcastCountries.add(countriesGroupedByCode.get(subdivision.countryCode)) + broadcastCountries.add(countriesKeyByCode.get(subdivision.countryCode)) } }) @@ -134,8 +144,8 @@ export class Feed { this.broadcastRegions = regions.filter((region: Region) => { if (region.code === 'INT') return false - - return region.countryCodes.intersects(countriesCodes) + const intersected = region.countryCodes.intersects(countriesCodes) + return intersected.notEmpty() }) return this @@ -197,4 +207,22 @@ export class Feed { return this.getBroadcastRegions().includes((_region: Region) => _region.code === region.code) } + + getGuides(): Collection { + if (!this.guides) return new Collection() + + return this.guides + } + + getStreams(): Collection { + if (!this.streams) return new Collection() + + return this.streams + } + + getFullName(): string { + if (!this.channel) return '' + + return `${this.channel.name} ${this.name}` + } } diff --git a/scripts/models/guide.ts b/scripts/models/guide.ts new file mode 100644 index 000000000..3bc849d86 --- /dev/null +++ b/scripts/models/guide.ts @@ -0,0 +1,54 @@ +import type { GuideData, GuideSerializedData } from '../types/guide' + +export class Guide { + channelId?: string + feedId?: string + siteDomain: string + siteId: string + siteName: string + languageCode: string + + constructor(data?: GuideData) { + if (!data) return + + this.channelId = data.channel + this.feedId = data.feed + this.siteDomain = data.site + this.siteId = data.site_id + this.siteName = data.site_name + this.languageCode = data.lang + } + + getUUID(): string { + return this.getStreamId() + this.siteId + } + + getStreamId(): string | undefined { + if (!this.channelId) return undefined + if (!this.feedId) return this.channelId + + return `${this.channelId}@${this.feedId}` + } + + serialize(): GuideSerializedData { + return { + channelId: this.channelId, + feedId: this.feedId, + siteDomain: this.siteDomain, + siteId: this.siteId, + siteName: this.siteName, + languageCode: this.languageCode + } + } + + deserialize(data: GuideSerializedData): this { + this.channelId = data.channelId + this.feedId = data.feedId + this.siteDomain = data.siteDomain + this.siteId = data.siteId + this.siteName = data.siteName + this.languageCode = data.languageCode + + return this + } +} diff --git a/scripts/models/index.ts b/scripts/models/index.ts index 83a9380ed..db4d6f5fa 100644 --- a/scripts/models/index.ts +++ b/scripts/models/index.ts @@ -1,13 +1,14 @@ -export * from './issue' -export * from './playlist' -export * from './blocked' -export * from './stream' +export * from './blocklistRecord' +export * from './broadcastArea' export * from './category' export * from './channel' -export * from './language' export * from './country' -export * from './region' -export * from './subdivision' export * from './feed' -export * from './broadcastArea' +export * from './guide' +export * from './issue' +export * from './language' +export * from './playlist' +export * from './region' +export * from './stream' +export * from './subdivision' export * from './timezone' diff --git a/scripts/models/language.ts b/scripts/models/language.ts index aeda5e6c2..1e6df829b 100644 --- a/scripts/models/language.ts +++ b/scripts/models/language.ts @@ -1,14 +1,27 @@ -type LanguageData = { - code: string - name: string -} +import type { LanguageData, LanguageSerializedData } from '../types/language' export class Language { code: string name: string - constructor(data: LanguageData) { + constructor(data?: LanguageData) { + if (!data) return + this.code = data.code this.name = data.name } + + serialize(): LanguageSerializedData { + return { + code: this.code, + name: this.name + } + } + + deserialize(data: LanguageSerializedData): this { + this.code = data.code + this.name = data.name + + return this + } } diff --git a/scripts/models/playlist.ts b/scripts/models/playlist.ts index ba0a25c5c..d3022a5af 100644 --- a/scripts/models/playlist.ts +++ b/scripts/models/playlist.ts @@ -17,10 +17,10 @@ export class Playlist { } toString() { - let output = '#EXTM3U\n' + let output = '#EXTM3U\r\n' this.streams.forEach((stream: Stream) => { - output += stream.toString(this.options) + '\n' + output += stream.toString(this.options) + '\r\n' }) return output diff --git a/scripts/models/region.ts b/scripts/models/region.ts index 928b48f06..ace44bc52 100644 --- a/scripts/models/region.ts +++ b/scripts/models/region.ts @@ -1,27 +1,26 @@ import { Collection, Dictionary } from '@freearhey/core' -import { Subdivision } from '.' - -type RegionData = { - code: string - name: string - countries: string[] -} +import { Country, Subdivision } from '.' +import type { RegionData, RegionSerializedData } from '../types/region' +import { CountrySerializedData } from '../types/country' +import { SubdivisionSerializedData } from '../types/subdivision' export class Region { code: string name: string countryCodes: Collection - countries?: Collection - subdivisions?: Collection + countries: Collection = new Collection() + subdivisions: Collection = new Collection() + + constructor(data?: RegionData) { + if (!data) return - constructor(data: RegionData) { this.code = data.code this.name = data.name this.countryCodes = new Collection(data.countries) } - withCountries(countriesGroupedByCode: Dictionary): this { - this.countries = this.countryCodes.map((code: string) => countriesGroupedByCode.get(code)) + withCountries(countriesKeyByCode: Dictionary): this { + this.countries = this.countryCodes.map((code: string) => countriesKeyByCode.get(code)) return this } @@ -35,11 +34,11 @@ export class Region { } getSubdivisions(): Collection { - return this.subdivisions || new Collection() + return this.subdivisions } getCountries(): Collection { - return this.countries || new Collection() + return this.countries } includesCountryCode(code: string): boolean { @@ -49,4 +48,30 @@ export class Region { isWorldwide(): boolean { return this.code === 'INT' } + + serialize(): RegionSerializedData { + return { + code: this.code, + name: this.name, + countryCodes: this.countryCodes.all(), + countries: this.countries.map((country: Country) => country.serialize()).all(), + subdivisions: this.subdivisions + .map((subdivision: Subdivision) => subdivision.serialize()) + .all() + } + } + + deserialize(data: RegionSerializedData): this { + this.code = data.code + this.name = data.name + this.countryCodes = new Collection(data.countryCodes) + this.countries = new Collection(data.countries).map((data: CountrySerializedData) => + new Country().deserialize(data) + ) + this.subdivisions = new Collection(data.subdivisions).map((data: SubdivisionSerializedData) => + new Subdivision().deserialize(data) + ) + + return this + } } diff --git a/scripts/models/stream.ts b/scripts/models/stream.ts index bd01845f6..443a249b9 100644 --- a/scripts/models/stream.ts +++ b/scripts/models/stream.ts @@ -1,26 +1,45 @@ -import { URL, Collection, Dictionary } from '@freearhey/core' import { Feed, Channel, Category, Region, Subdivision, Country, Language } from './index' +import { URL, Collection, Dictionary } from '@freearhey/core' +import type { StreamData } from '../types/stream' import parser from 'iptv-playlist-parser' export class Stream { - name: string + name?: string url: string id?: string - groupTitle: string channelId?: string channel?: Channel feedId?: string feed?: Feed filepath?: string - line: number + line?: number label?: string verticalResolution?: number isInterlaced?: boolean - httpReferrer?: string - httpUserAgent?: string + referrer?: string + userAgent?: string + groupTitle: string = 'Undefined' removed: boolean = false - constructor(data: parser.PlaylistItem) { + constructor(data?: StreamData) { + if (!data) return + + const id = data.channel && data.feed ? [data.channel, data.feed].join('@') : data.channel + const { verticalResolution, isInterlaced } = parseQuality(data.quality) + + this.id = id || undefined + this.channelId = data.channel || undefined + this.feedId = data.feed || undefined + this.name = data.name || undefined + this.url = data.url + this.referrer = data.referrer || undefined + this.userAgent = data.user_agent || undefined + this.verticalResolution = verticalResolution || undefined + this.isInterlaced = isInterlaced || undefined + this.label = data.label || undefined + } + + fromPlaylistItem(data: parser.PlaylistItem): this { if (!data.name) throw new Error('"name" property is required') if (!data.url) throw new Error('"url" property is required') @@ -37,15 +56,16 @@ export class Stream { this.verticalResolution = verticalResolution || undefined this.isInterlaced = isInterlaced || undefined this.url = data.url - this.httpReferrer = data.http.referrer || undefined - this.httpUserAgent = data.http['user-agent'] || undefined - this.groupTitle = 'Undefined' + this.referrer = data.http.referrer || undefined + this.userAgent = data.http['user-agent'] || undefined + + return this } - withChannel(channelsGroupedById: Dictionary): this { + withChannel(channelsKeyById: Dictionary): this { if (!this.channelId) return this - this.channel = channelsGroupedById.get(this.channelId) + this.channel = channelsKeyById.get(this.channelId) return this } @@ -93,18 +113,22 @@ export class Stream { return this } - setHttpUserAgent(httpUserAgent: string): this { - this.httpUserAgent = httpUserAgent + setUserAgent(userAgent: string): this { + this.userAgent = userAgent return this } - setHttpReferrer(httpReferrer: string): this { - this.httpReferrer = httpReferrer + setReferrer(referrer: string): this { + this.referrer = referrer return this } + getLine(): number { + return this.line || -1 + } + setFilepath(filepath: string): this { this.filepath = filepath @@ -133,12 +157,12 @@ export class Stream { return this.filepath || '' } - getHttpReferrer(): string { - return this.httpReferrer || '' + getReferrer(): string { + return this.referrer || '' } - getHttpUserAgent(): string { - return this.httpUserAgent || '' + getUserAgent(): string { + return this.userAgent || '' } getQuality(): string { @@ -198,14 +222,6 @@ export class Stream { return Object.assign(Object.create(Object.getPrototypeOf(this)), this) } - hasName(): boolean { - return !!this.name - } - - noName(): boolean { - return !this.name - } - hasChannel() { return !!this.channel } @@ -281,8 +297,12 @@ export class Stream { return this?.channel?.logo || '' } + getName(): string { + return this.name || '' + } + getTitle(): string { - let title = `${this.name}` + let title = `${this.getName()}` if (this.getQuality()) { title += ` (${this.getQuality()})` @@ -303,30 +323,13 @@ export class Stream { return this.id || '' } - data() { - return { - id: this.id, - channel: this.channel, - feed: this.feed, - filepath: this.filepath, - label: this.label, - name: this.name, - verticalResolution: this.verticalResolution, - isInterlaced: this.isInterlaced, - url: this.url, - httpReferrer: this.httpReferrer, - httpUserAgent: this.httpUserAgent, - line: this.line - } - } - toJSON() { return { channel: this.channelId || null, feed: this.feedId || null, url: this.url, - referrer: this.httpReferrer || null, - user_agent: this.httpUserAgent || null, + referrer: this.referrer || null, + user_agent: this.userAgent || null, quality: this.getQuality() || null } } @@ -338,25 +341,25 @@ export class Stream { output += ` tvg-logo="${this.getLogo()}" group-title="${this.groupTitle}"` } - if (this.httpReferrer) { - output += ` http-referrer="${this.httpReferrer}"` + if (this.referrer) { + output += ` http-referrer="${this.referrer}"` } - if (this.httpUserAgent) { - output += ` http-user-agent="${this.httpUserAgent}"` + if (this.userAgent) { + output += ` http-user-agent="${this.userAgent}"` } output += `,${this.getTitle()}` - if (this.httpReferrer) { - output += `\n#EXTVLCOPT:http-referrer=${this.httpReferrer}` + if (this.referrer) { + output += `\r\n#EXTVLCOPT:http-referrer=${this.referrer}` } - if (this.httpUserAgent) { - output += `\n#EXTVLCOPT:http-user-agent=${this.httpUserAgent}` + if (this.userAgent) { + output += `\r\n#EXTVLCOPT:http-user-agent=${this.userAgent}` } - output += `\n${this.url}` + output += `\r\n${this.url}` return output } @@ -379,7 +382,11 @@ function escapeRegExp(text) { return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&') } -function parseQuality(quality: string): { verticalResolution: number; isInterlaced: boolean } { +function parseQuality(quality: string | null): { + verticalResolution: number | null + isInterlaced: boolean | null +} { + if (!quality) return { verticalResolution: null, isInterlaced: null } let [, verticalResolutionString] = quality.match(/^(\d+)/) || [null, undefined] const isInterlaced = /i$/i.test(quality) let verticalResolution = 0 diff --git a/scripts/models/subdivision.ts b/scripts/models/subdivision.ts index d6795fea3..b43d1c88d 100644 --- a/scripts/models/subdivision.ts +++ b/scripts/models/subdivision.ts @@ -1,26 +1,41 @@ +import { SubdivisionData, SubdivisionSerializedData } from '../types/subdivision' import { Dictionary } from '@freearhey/core' import { Country } from '.' -type SubdivisionData = { - code: string - name: string - country: string -} - export class Subdivision { code: string name: string countryCode: string country?: Country - constructor(data: SubdivisionData) { + constructor(data?: SubdivisionData) { + if (!data) return + this.code = data.code this.name = data.name this.countryCode = data.country } - withCountry(countriesGroupedByCode: Dictionary): this { - this.country = countriesGroupedByCode.get(this.countryCode) + withCountry(countriesKeyByCode: Dictionary): this { + this.country = countriesKeyByCode.get(this.countryCode) + + return this + } + + serialize(): SubdivisionSerializedData { + return { + code: this.code, + name: this.name, + countryCode: this.code, + country: this.country ? this.country.serialize() : undefined + } + } + + deserialize(data: SubdivisionSerializedData): this { + this.code = data.code + this.name = data.name + this.countryCode = data.countryCode + this.country = data.country ? new Country().deserialize(data.country) : undefined return this } diff --git a/scripts/models/timezone.ts b/scripts/models/timezone.ts index b519f0e06..e4071138f 100644 --- a/scripts/models/timezone.ts +++ b/scripts/models/timezone.ts @@ -18,8 +18,8 @@ export class Timezone { this.countryCodes = new Collection(data.countries) } - withCountries(countriesGroupedByCode: Dictionary): this { - this.countries = this.countryCodes.map((code: string) => countriesGroupedByCode.get(code)) + withCountries(countriesKeyByCode: Dictionary): this { + this.countries = this.countryCodes.map((code: string) => countriesKeyByCode.get(code)) return this } diff --git a/scripts/types/blocklistRecord.d.ts b/scripts/types/blocklistRecord.d.ts new file mode 100644 index 000000000..4b1d9e7dc --- /dev/null +++ b/scripts/types/blocklistRecord.d.ts @@ -0,0 +1,5 @@ +export type BlocklistRecordData = { + channel: string + reason: string + ref: string +} diff --git a/scripts/types/category.d.ts b/scripts/types/category.d.ts new file mode 100644 index 000000000..e78d6c62e --- /dev/null +++ b/scripts/types/category.d.ts @@ -0,0 +1,9 @@ +export type CategorySerializedData = { + id: string + name: string +} + +export type CategoryData = { + id: string + name: string +} diff --git a/scripts/types/channel.d.ts b/scripts/types/channel.d.ts new file mode 100644 index 000000000..1f9d031cb --- /dev/null +++ b/scripts/types/channel.d.ts @@ -0,0 +1,52 @@ +import { Collection } from '@freearhey/core' +import type { CountrySerializedData } from './country' +import type { SubdivisionSerializedData } from './subdivision' +import type { CategorySerializedData } from './category' + +export type ChannelSerializedData = { + id: string + name: string + altNames: string[] + network?: string + owners: string[] + countryCode: string + country?: CountrySerializedData + subdivisionCode?: string + subdivision?: SubdivisionSerializedData + cityName?: string + categoryIds: string[] + categories?: CategorySerializedData[] + isNSFW: boolean + launched?: string + closed?: string + replacedBy?: string + website?: string + logo: string +} + +export type ChannelData = { + id: string + name: string + alt_names: string[] + network: string + owners: Collection + country: string + subdivision: string + city: string + categories: Collection + is_nsfw: boolean + launched: string + closed: string + replaced_by: string + website: string + logo: string +} + +export type ChannelSearchableData = { + id: string + name: string + altNames: string[] + guideNames: string[] + streamNames: string[] + feedFullNames: string[] +} diff --git a/scripts/types/country.d.ts b/scripts/types/country.d.ts new file mode 100644 index 000000000..9554d4c68 --- /dev/null +++ b/scripts/types/country.d.ts @@ -0,0 +1,20 @@ +import type { LanguageSerializedData } from './language' +import type { SubdivisionSerializedData } from './subdivision' +import type { RegionSerializedData } from './region' + +export type CountrySerializedData = { + code: string + name: string + flag: string + languageCode: string + language: LanguageSerializedData | null + subdivisions: SubdivisionSerializedData[] + regions: RegionSerializedData[] +} + +export type CountryData = { + code: string + name: string + lang: string + flag: string +} diff --git a/scripts/types/dataLoader.d.ts b/scripts/types/dataLoader.d.ts new file mode 100644 index 000000000..05742ff9d --- /dev/null +++ b/scripts/types/dataLoader.d.ts @@ -0,0 +1,19 @@ +import { Storage } from '@freearhey/core' + +export type DataLoaderProps = { + storage: Storage +} + +export type DataLoaderData = { + countries: object | object[] + regions: object | object[] + subdivisions: object | object[] + languages: object | object[] + categories: object | object[] + blocklist: object | object[] + channels: object | object[] + feeds: object | object[] + timezones: object | object[] + guides: object | object[] + streams: object | object[] +} diff --git a/scripts/types/dataProcessor.d.ts b/scripts/types/dataProcessor.d.ts new file mode 100644 index 000000000..1005ff5b2 --- /dev/null +++ b/scripts/types/dataProcessor.d.ts @@ -0,0 +1,27 @@ +import { Collection, Dictionary } from '@freearhey/core' + +export type DataProcessorData = { + blocklistRecordsGroupedByChannelId: Dictionary + subdivisionsGroupedByCountryCode: Dictionary + feedsGroupedByChannelId: Dictionary + guidesGroupedByStreamId: Dictionary + subdivisionsKeyByCode: Dictionary + countriesKeyByCode: Dictionary + languagesKeyByCode: Dictionary + streamsGroupedById: Dictionary + categoriesKeyById: Dictionary + timezonesKeyById: Dictionary + regionsKeyByCode: Dictionary + blocklistRecords: Collection + channelsKeyById: Dictionary + subdivisions: Collection + categories: Collection + countries: Collection + languages: Collection + timezones: Collection + channels: Collection + regions: Collection + streams: Collection + guides: Collection + feeds: Collection +} diff --git a/scripts/types/feed.d.ts b/scripts/types/feed.d.ts new file mode 100644 index 000000000..5c6722dde --- /dev/null +++ b/scripts/types/feed.d.ts @@ -0,0 +1,12 @@ +import { Collection } from '@freearhey/core' + +export type FeedData = { + channel: string + id: string + name: string + is_main: boolean + broadcast_area: Collection + languages: Collection + timezones: Collection + video_format: string +} diff --git a/scripts/types/guide.d.ts b/scripts/types/guide.d.ts new file mode 100644 index 000000000..63a6ecdb1 --- /dev/null +++ b/scripts/types/guide.d.ts @@ -0,0 +1,17 @@ +export type GuideSerializedData = { + channelId?: string + feedId?: string + siteDomain: string + siteId: string + siteName: string + languageCode: string +} + +export type GuideData = { + channel: string + feed: string + site: string + site_id: string + site_name: string + lang: string +} diff --git a/scripts/types/language.d.ts b/scripts/types/language.d.ts new file mode 100644 index 000000000..2b9d4525c --- /dev/null +++ b/scripts/types/language.d.ts @@ -0,0 +1,9 @@ +export type LanguageSerializedData = { + code: string + name: string +} + +export type LanguageData = { + code: string + name: string +} diff --git a/scripts/types/region.d.ts b/scripts/types/region.d.ts new file mode 100644 index 000000000..e6773429e --- /dev/null +++ b/scripts/types/region.d.ts @@ -0,0 +1,13 @@ +export type RegionSerializedData = { + code: string + name: string + countryCodes: string[] + countries?: CountrySerializedData[] + subdivisions?: SubdivisionSerializedData[] +} + +export type RegionData = { + code: string + name: string + countries: string[] +} diff --git a/scripts/types/stream.d.ts b/scripts/types/stream.d.ts new file mode 100644 index 000000000..667ad2586 --- /dev/null +++ b/scripts/types/stream.d.ts @@ -0,0 +1,10 @@ +export type StreamData = { + channel: string | null + feed: string | null + name: string | null + url: string + referrer: string | null + user_agent: string | null + quality: string | null + label: string | null +} diff --git a/scripts/types/subdivision.d.ts b/scripts/types/subdivision.d.ts new file mode 100644 index 000000000..bf46831f7 --- /dev/null +++ b/scripts/types/subdivision.d.ts @@ -0,0 +1,12 @@ +export type SubdivisionSerializedData = { + code: string + name: string + countryCode: string + country?: CountrySerializedData +} + +export type SubdivisionData = { + code: string + name: string + country: string +} diff --git a/tests/__data__/expected/api_generate/.api/streams.json b/tests/__data__/expected/api_generate/.api/streams.json new file mode 100644 index 000000000..55128bca3 --- /dev/null +++ b/tests/__data__/expected/api_generate/.api/streams.json @@ -0,0 +1,57 @@ +[ + { + "channel": null, + "feed": null, + "url": "http://51.15.246.58:8081/daawahtv/daawahtv2/playlist.m3u8", + "referrer": null, + "user_agent": null + }, + { + "channel": null, + "feed": null, + "url": "http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index2.m3u8", + "referrer": "http://imn.iq", + "user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148" + }, + { + "channel": "AndorraTV.ad", + "feed": "SD", + "url": "https://iptv-all.lanesh4d0w.repl.co/andorra/atv", + "referrer": null, + "user_agent": null + }, + { + "channel": "BBCNews.uk", + "url": "http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8", + "referrer": null, + "user_agent": null + }, + { + "channel": "LDPRTV.ru", + "feed": null, + "url": "http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8", + "referrer": null, + "user_agent": null + }, + { + "channel": "MeteoMedia.ca", + "feed": null, + "url": "http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8", + "referrer": null, + "user_agent": null + }, + { + "channel": "VisitXTV.nl", + "feed": null, + "url": "https://stream.visit-x.tv/vxtv/ngrp:live_all/30fps.m3u8", + "referrer": null, + "user_agent": null + }, + { + "channel": "Zoo.ad", + "feed": null, + "url": "https://iptv-all.lanesh4d0w.repl.co/andorra/zoo", + "referrer": null, + "user_agent": null + } +] \ No newline at end of file diff --git a/tests/__data__/expected/playlist_format/in.m3u b/tests/__data__/expected/playlist_format/in.m3u index 44a00ace1..2615da4e4 100644 --- a/tests/__data__/expected/playlist_format/in.m3u +++ b/tests/__data__/expected/playlist_format/in.m3u @@ -1,3 +1,3 @@ -#EXTM3U -#EXTINF:-1 tvg-id="",Manorama News -2 [U3] (480p) [Geo-blocked] [Not 24/7] -https://ythls.onrender.com/channel/UCP0uG-mcMImgKnJz-VjJZmQ.m3u8 +#EXTM3U +#EXTINF:-1 tvg-id="",Manorama News -2 [U3] (480p) [Geo-blocked] [Not 24/7] +https://ythls.onrender.com/channel/UCP0uG-mcMImgKnJz-VjJZmQ.m3u8 diff --git a/tests/__data__/expected/playlist_format/nl.m3u b/tests/__data__/expected/playlist_format/nl.m3u index 8f1e0ba2a..d08a2ac80 100644 --- a/tests/__data__/expected/playlist_format/nl.m3u +++ b/tests/__data__/expected/playlist_format/nl.m3u @@ -1,11 +1,11 @@ -#EXTM3U -#EXTINF:-1 tvg-id="NPO1.nl@SD",NPO 1 (342p) [Geo-blocked] -http://resolver.streaming.api.nos.nl/livestream?url=/live/npo/tvlive/npo1/npo1.isml/.m3u8 -#EXTINF:-1 tvg-id="NPO2.nl",NPO 2 (342p) -http://resolver.streaming.api.nos.nl/livestream?url=/live/npo/tvlive/npo2/npo2.isml/.m3u8 -#EXTINF:-1 tvg-id="NPO2.nl" http-referrer="http://imn.iq" http-user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",NPO 2 (302p) [Geo-blocked] -#EXTVLCOPT:http-referrer=http://imn.iq -#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 -http://stream.tvtap.net:8081/live/nl-npo2.stream/playlist.m3u8 -#EXTINF:-1 tvg-id="NPO2.nl",NPO 2 [Geo-blocked] -http://resolver.streaming.api.nos.nl/livestream?url=/live/npo/tvlive/npo2/npo22.isml/.m3u8 +#EXTM3U +#EXTINF:-1 tvg-id="NPO1.nl@SD",NPO 1 (342p) [Geo-blocked] +http://resolver.streaming.api.nos.nl/livestream?url=/live/npo/tvlive/npo1/npo1.isml/.m3u8 +#EXTINF:-1 tvg-id="NPO2.nl",NPO 2 (342p) +http://resolver.streaming.api.nos.nl/livestream?url=/live/npo/tvlive/npo2/npo2.isml/.m3u8 +#EXTINF:-1 tvg-id="NPO2.nl" http-referrer="http://imn.iq" http-user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",NPO 2 (302p) [Geo-blocked] +#EXTVLCOPT:http-referrer=http://imn.iq +#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 +http://stream.tvtap.net:8081/live/nl-npo2.stream/playlist.m3u8 +#EXTINF:-1 tvg-id="NPO2.nl",NPO 2 [Geo-blocked] +http://resolver.streaming.api.nos.nl/livestream?url=/live/npo/tvlive/npo2/npo22.isml/.m3u8 diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/categories/general.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/categories/general.m3u new file mode 100644 index 000000000..d43fa35f6 --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/categories/general.m3u @@ -0,0 +1,7 @@ +#EXTM3U +#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD +http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8 +#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p) +http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8 +#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="General",ЭлТР (480p) [Not 24/7] +http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8 diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/categories/legislative.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/categories/legislative.m3u new file mode 100644 index 000000000..7452e53aa --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/categories/legislative.m3u @@ -0,0 +1 @@ +#EXTM3U diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/categories/news.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/categories/news.m3u new file mode 100644 index 000000000..85c75b78b --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/categories/news.m3u @@ -0,0 +1,3 @@ +#EXTM3U +#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD +http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8 diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/categories/undefined.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/categories/undefined.m3u new file mode 100644 index 000000000..cfdce7d0b --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/categories/undefined.m3u @@ -0,0 +1,15 @@ +#EXTM3U +#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined" http-referrer="http://imn.iq" http-user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",Andorra TV (720p) [Not 24/7] +#EXTVLCOPT:http-referrer=http://imn.iq +#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 +http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index2.m3u8 +#EXTINF:-1 tvg-id="AndorraTV.ad@SD" tvg-logo="" group-title="Undefined",ATV +https://iptv-all.lanesh4d0w.repl.co/andorra/atv +#EXTINF:-1 tvg-id="AndorraTV.ad@HD" tvg-logo="" group-title="Undefined",ATV HD +https://iptv-all.lanesh4d0w.repl.co/andorra/atv_hd +#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined",Daawah TV +http://51.15.246.58:8081/daawahtv/daawahtv2/playlist.m3u8 +#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i) +http://146.59.85.40:89/dunaworld/index.m3u8 +#EXTINF:-1 tvg-id="Zoo.ad@HD" tvg-logo="" group-title="Undefined",Zoo (720p) +https://iptv-all.lanesh4d0w.repl.co/andorra/zoo diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/categories/weather.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/categories/weather.m3u new file mode 100644 index 000000000..03a6963a3 --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/categories/weather.m3u @@ -0,0 +1,3 @@ +#EXTM3U +#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Weather",Meteomedia +http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8 diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/categories/xxx.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/categories/xxx.m3u new file mode 100644 index 000000000..a6791d64b --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/categories/xxx.m3u @@ -0,0 +1,3 @@ +#EXTM3U +#EXTINF:-1 tvg-id="VisitXTV.nl" tvg-logo="https://i.imgur.com/RJ9wbNF.jpg" group-title="XXX",Visit-X TV +https://stream.visit-x.tv/vxtv/ngrp:live_all/30fps.m3u8 diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/countries/ad.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/countries/ad.m3u new file mode 100644 index 000000000..9bc95be27 --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/countries/ad.m3u @@ -0,0 +1,3 @@ +#EXTM3U +#EXTINF:-1 tvg-id="AndorraTV.ad@SD" tvg-logo="" group-title="Undefined",ATV +https://iptv-all.lanesh4d0w.repl.co/andorra/atv diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/countries/ca.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/countries/ca.m3u new file mode 100644 index 000000000..03a6963a3 --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/countries/ca.m3u @@ -0,0 +1,3 @@ +#EXTM3U +#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Weather",Meteomedia +http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8 diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/countries/ru.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/countries/ru.m3u new file mode 100644 index 000000000..b701890a3 --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/countries/ru.m3u @@ -0,0 +1,3 @@ +#EXTM3U +#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p) +http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8 diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/countries/undefined.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/countries/undefined.m3u new file mode 100644 index 000000000..eb0364cd5 --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/countries/undefined.m3u @@ -0,0 +1,11 @@ +#EXTM3U +#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined" http-referrer="http://imn.iq" http-user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",Andorra TV (720p) [Not 24/7] +#EXTVLCOPT:http-referrer=http://imn.iq +#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 +http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index2.m3u8 +#EXTINF:-1 tvg-id="AndorraTV.ad@HD" tvg-logo="" group-title="Undefined",ATV HD +https://iptv-all.lanesh4d0w.repl.co/andorra/atv_hd +#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined",Daawah TV +http://51.15.246.58:8081/daawahtv/daawahtv2/playlist.m3u8 +#EXTINF:-1 tvg-id="Zoo.ad@HD" tvg-logo="" group-title="Undefined",Zoo (720p) +https://iptv-all.lanesh4d0w.repl.co/andorra/zoo diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/index.category.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/index.category.m3u new file mode 100644 index 000000000..b3235a876 --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/index.category.m3u @@ -0,0 +1,25 @@ +#EXTM3U +#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General",BBC News HD +http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8 +#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p) +http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8 +#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="General",ЭлТР (480p) [Not 24/7] +http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8 +#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="News",BBC News HD +http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8 +#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Weather",Meteomedia +http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8 +#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined" http-referrer="http://imn.iq" http-user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",Andorra TV (720p) [Not 24/7] +#EXTVLCOPT:http-referrer=http://imn.iq +#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 +http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index2.m3u8 +#EXTINF:-1 tvg-id="AndorraTV.ad@SD" tvg-logo="" group-title="Undefined",ATV +https://iptv-all.lanesh4d0w.repl.co/andorra/atv +#EXTINF:-1 tvg-id="AndorraTV.ad@HD" tvg-logo="" group-title="Undefined",ATV HD +https://iptv-all.lanesh4d0w.repl.co/andorra/atv_hd +#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined",Daawah TV +http://51.15.246.58:8081/daawahtv/daawahtv2/playlist.m3u8 +#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i) +http://146.59.85.40:89/dunaworld/index.m3u8 +#EXTINF:-1 tvg-id="Zoo.ad@HD" tvg-logo="" group-title="Undefined",Zoo (720p) +https://iptv-all.lanesh4d0w.repl.co/andorra/zoo diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/index.country.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/index.country.m3u new file mode 100644 index 000000000..332df0029 --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/index.country.m3u @@ -0,0 +1,31 @@ +#EXTM3U +#EXTINF:-1 tvg-id="AndorraTV.ad@SD" tvg-logo="" group-title="Andorra",ATV +https://iptv-all.lanesh4d0w.repl.co/andorra/atv +#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Canada",Meteomedia +http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8 +#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Kazakhstan",ЭлТР (480p) [Not 24/7] +http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8 +#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Kyrgyzstan",ЭлТР (480p) [Not 24/7] +http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8 +#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="Russia",ЛДПР ТВ (1080p) +http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8 +#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Tajikistan",ЭлТР (480p) [Not 24/7] +http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8 +#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Turkmenistan",ЭлТР (480p) [Not 24/7] +http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8 +#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Uzbekistan",ЭлТР (480p) [Not 24/7] +http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8 +#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="International",BBC News HD +http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8 +#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="International",Duna World (576i) +http://146.59.85.40:89/dunaworld/index.m3u8 +#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined" http-referrer="http://imn.iq" http-user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",Andorra TV (720p) [Not 24/7] +#EXTVLCOPT:http-referrer=http://imn.iq +#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 +http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index2.m3u8 +#EXTINF:-1 tvg-id="AndorraTV.ad@HD" tvg-logo="" group-title="Undefined",ATV HD +https://iptv-all.lanesh4d0w.repl.co/andorra/atv_hd +#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined",Daawah TV +http://51.15.246.58:8081/daawahtv/daawahtv2/playlist.m3u8 +#EXTINF:-1 tvg-id="Zoo.ad@HD" tvg-logo="" group-title="Undefined",Zoo (720p) +https://iptv-all.lanesh4d0w.repl.co/andorra/zoo diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/index.language.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/index.language.m3u new file mode 100644 index 000000000..e9f88f8ac --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/index.language.m3u @@ -0,0 +1,23 @@ +#EXTM3U +#EXTINF:-1 tvg-id="AndorraTV.ad@SD" tvg-logo="" group-title="Catalan",ATV +https://iptv-all.lanesh4d0w.repl.co/andorra/atv +#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="English",BBC News HD +http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8 +#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="Russian",ЛДПР ТВ (1080p) +http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8 +#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined" http-referrer="http://imn.iq" http-user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",Andorra TV (720p) [Not 24/7] +#EXTVLCOPT:http-referrer=http://imn.iq +#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 +http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index2.m3u8 +#EXTINF:-1 tvg-id="AndorraTV.ad@HD" tvg-logo="" group-title="Undefined",ATV HD +https://iptv-all.lanesh4d0w.repl.co/andorra/atv_hd +#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined",Daawah TV +http://51.15.246.58:8081/daawahtv/daawahtv2/playlist.m3u8 +#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i) +http://146.59.85.40:89/dunaworld/index.m3u8 +#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Undefined",Meteomedia +http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8 +#EXTINF:-1 tvg-id="Zoo.ad@HD" tvg-logo="" group-title="Undefined",Zoo (720p) +https://iptv-all.lanesh4d0w.repl.co/andorra/zoo +#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Undefined",ЭлТР (480p) [Not 24/7] +http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8 diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/index.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/index.m3u new file mode 100644 index 000000000..fbf8cd755 --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/index.m3u @@ -0,0 +1,23 @@ +#EXTM3U +#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined" http-referrer="http://imn.iq" http-user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",Andorra TV (720p) [Not 24/7] +#EXTVLCOPT:http-referrer=http://imn.iq +#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 +http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index2.m3u8 +#EXTINF:-1 tvg-id="AndorraTV.ad@SD" tvg-logo="" group-title="Undefined",ATV +https://iptv-all.lanesh4d0w.repl.co/andorra/atv +#EXTINF:-1 tvg-id="AndorraTV.ad@HD" tvg-logo="" group-title="Undefined",ATV HD +https://iptv-all.lanesh4d0w.repl.co/andorra/atv_hd +#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD +http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8 +#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined",Daawah TV +http://51.15.246.58:8081/daawahtv/daawahtv2/playlist.m3u8 +#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i) +http://146.59.85.40:89/dunaworld/index.m3u8 +#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Weather",Meteomedia +http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8 +#EXTINF:-1 tvg-id="Zoo.ad@HD" tvg-logo="" group-title="Undefined",Zoo (720p) +https://iptv-all.lanesh4d0w.repl.co/andorra/zoo +#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p) +http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8 +#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="General",ЭлТР (480p) [Not 24/7] +http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8 diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/index.region.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/index.region.m3u new file mode 100644 index 000000000..aa3534ff7 --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/index.region.m3u @@ -0,0 +1,43 @@ +#EXTM3U +#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Americas",Meteomedia +http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8 +#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="Asia",ЛДПР ТВ (1080p) +http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8 +#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Asia",ЭлТР (480p) [Not 24/7] +http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8 +#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Central Asia",ЭлТР (480p) [Not 24/7] +http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8 +#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="Commonwealth of Independent States",ЛДПР ТВ (1080p) +http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8 +#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Commonwealth of Independent States",ЭлТР (480p) [Not 24/7] +http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8 +#EXTINF:-1 tvg-id="AndorraTV.ad@SD" tvg-logo="" group-title="Europe",ATV +https://iptv-all.lanesh4d0w.repl.co/andorra/atv +#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="Europe",ЛДПР ТВ (1080p) +http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8 +#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Europe",ЭлТР (480p) [Not 24/7] +http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8 +#EXTINF:-1 tvg-id="AndorraTV.ad@SD" tvg-logo="" group-title="Europe, the Middle East and Africa",ATV +https://iptv-all.lanesh4d0w.repl.co/andorra/atv +#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="Europe, the Middle East and Africa",ЛДПР ТВ (1080p) +http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8 +#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="Europe, the Middle East and Africa",ЭлТР (480p) [Not 24/7] +http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8 +#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="North America",Meteomedia +http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8 +#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Northern America",Meteomedia +http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8 +#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="International",BBC News HD +http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8 +#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="International",Duna World (576i) +http://146.59.85.40:89/dunaworld/index.m3u8 +#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined" http-referrer="http://imn.iq" http-user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",Andorra TV (720p) [Not 24/7] +#EXTVLCOPT:http-referrer=http://imn.iq +#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 +http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index2.m3u8 +#EXTINF:-1 tvg-id="AndorraTV.ad@HD" tvg-logo="" group-title="Undefined",ATV HD +https://iptv-all.lanesh4d0w.repl.co/andorra/atv_hd +#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined",Daawah TV +http://51.15.246.58:8081/daawahtv/daawahtv2/playlist.m3u8 +#EXTINF:-1 tvg-id="Zoo.ad@HD" tvg-logo="" group-title="Undefined",Zoo (720p) +https://iptv-all.lanesh4d0w.repl.co/andorra/zoo diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/languages/eng.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/languages/eng.m3u new file mode 100644 index 000000000..85c75b78b --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/languages/eng.m3u @@ -0,0 +1,3 @@ +#EXTM3U +#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD +http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8 diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/languages/rus.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/languages/rus.m3u new file mode 100644 index 000000000..b701890a3 --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/languages/rus.m3u @@ -0,0 +1,3 @@ +#EXTM3U +#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p) +http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8 diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/languages/undefined.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/languages/undefined.m3u new file mode 100644 index 000000000..0eeed846f --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/languages/undefined.m3u @@ -0,0 +1,17 @@ +#EXTM3U +#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined" http-referrer="http://imn.iq" http-user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",Andorra TV (720p) [Not 24/7] +#EXTVLCOPT:http-referrer=http://imn.iq +#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 +http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index2.m3u8 +#EXTINF:-1 tvg-id="AndorraTV.ad@HD" tvg-logo="" group-title="Undefined",ATV HD +https://iptv-all.lanesh4d0w.repl.co/andorra/atv_hd +#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined",Daawah TV +http://51.15.246.58:8081/daawahtv/daawahtv2/playlist.m3u8 +#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i) +http://146.59.85.40:89/dunaworld/index.m3u8 +#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Weather",Meteomedia +http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8 +#EXTINF:-1 tvg-id="Zoo.ad@HD" tvg-logo="" group-title="Undefined",Zoo (720p) +https://iptv-all.lanesh4d0w.repl.co/andorra/zoo +#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="General",ЭлТР (480p) [Not 24/7] +http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8 diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/afr.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/afr.m3u new file mode 100644 index 000000000..7452e53aa --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/afr.m3u @@ -0,0 +1 @@ +#EXTM3U diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/amer.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/amer.m3u new file mode 100644 index 000000000..03a6963a3 --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/amer.m3u @@ -0,0 +1,3 @@ +#EXTM3U +#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Weather",Meteomedia +http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8 diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/apac.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/apac.m3u new file mode 100644 index 000000000..7452e53aa --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/apac.m3u @@ -0,0 +1 @@ +#EXTM3U diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/arab.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/arab.m3u new file mode 100644 index 000000000..7452e53aa --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/arab.m3u @@ -0,0 +1 @@ +#EXTM3U diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/asia.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/asia.m3u new file mode 100644 index 000000000..1a2c778e0 --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/asia.m3u @@ -0,0 +1,5 @@ +#EXTM3U +#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p) +http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8 +#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="General",ЭлТР (480p) [Not 24/7] +http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8 diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/carib.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/carib.m3u new file mode 100644 index 000000000..7452e53aa --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/carib.m3u @@ -0,0 +1 @@ +#EXTM3U diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/cas.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/cas.m3u new file mode 100644 index 000000000..847591ab0 --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/cas.m3u @@ -0,0 +1,3 @@ +#EXTM3U +#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="General",ЭлТР (480p) [Not 24/7] +http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8 diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/cis.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/cis.m3u new file mode 100644 index 000000000..1a2c778e0 --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/cis.m3u @@ -0,0 +1,5 @@ +#EXTM3U +#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p) +http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8 +#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="General",ЭлТР (480p) [Not 24/7] +http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8 diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/emea.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/emea.m3u new file mode 100644 index 000000000..10fbb43f9 --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/emea.m3u @@ -0,0 +1,7 @@ +#EXTM3U +#EXTINF:-1 tvg-id="AndorraTV.ad@SD" tvg-logo="" group-title="Undefined",ATV +https://iptv-all.lanesh4d0w.repl.co/andorra/atv +#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p) +http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8 +#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="General",ЭлТР (480p) [Not 24/7] +http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8 diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/eur.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/eur.m3u new file mode 100644 index 000000000..10fbb43f9 --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/eur.m3u @@ -0,0 +1,7 @@ +#EXTM3U +#EXTINF:-1 tvg-id="AndorraTV.ad@SD" tvg-logo="" group-title="Undefined",ATV +https://iptv-all.lanesh4d0w.repl.co/andorra/atv +#EXTINF:-1 tvg-id="LDPRTV.ru" tvg-logo="https://iptvx.one/icn/ldpr-tv.png" group-title="General",ЛДПР ТВ (1080p) +http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8 +#EXTINF:-1 tvg-id="ElTR.kg" tvg-logo="https://i.ibb.co/r6czQwQ/365049798-774721644658455-5702658175909463406-n-2.png" group-title="General",ЭлТР (480p) [Not 24/7] +http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8 diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/hispam.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/hispam.m3u new file mode 100644 index 000000000..7452e53aa --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/hispam.m3u @@ -0,0 +1 @@ +#EXTM3U diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/int.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/int.m3u new file mode 100644 index 000000000..15240af7d --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/int.m3u @@ -0,0 +1,5 @@ +#EXTM3U +#EXTINF:-1 tvg-id="BBCNews.uk" tvg-logo="https://raw.githubusercontent.com/Tapiosinn/tv-logos/master/countries/united-kingdom/bbc-news-uk.png" group-title="General;News",BBC News HD +http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8 +#EXTINF:-1 tvg-id="DunaWorld.hu" tvg-logo="https://i.imgur.com/uOBQJZS.png" group-title="Undefined",Duna World (576i) +http://146.59.85.40:89/dunaworld/index.m3u8 diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/lac.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/lac.m3u new file mode 100644 index 000000000..7452e53aa --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/lac.m3u @@ -0,0 +1 @@ +#EXTM3U diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/latam.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/latam.m3u new file mode 100644 index 000000000..7452e53aa --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/latam.m3u @@ -0,0 +1 @@ +#EXTM3U diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/maghreb.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/maghreb.m3u new file mode 100644 index 000000000..7452e53aa --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/maghreb.m3u @@ -0,0 +1 @@ +#EXTM3U diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/mena.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/mena.m3u new file mode 100644 index 000000000..7452e53aa --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/mena.m3u @@ -0,0 +1 @@ +#EXTM3U diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/mideast.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/mideast.m3u new file mode 100644 index 000000000..7452e53aa --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/mideast.m3u @@ -0,0 +1 @@ +#EXTM3U diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/nam.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/nam.m3u new file mode 100644 index 000000000..03a6963a3 --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/nam.m3u @@ -0,0 +1,3 @@ +#EXTM3U +#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Weather",Meteomedia +http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8 diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/noram.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/noram.m3u new file mode 100644 index 000000000..03a6963a3 --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/noram.m3u @@ -0,0 +1,3 @@ +#EXTM3U +#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Weather",Meteomedia +http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8 diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/nord.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/nord.m3u new file mode 100644 index 000000000..7452e53aa --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/nord.m3u @@ -0,0 +1 @@ +#EXTM3U diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/oce.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/oce.m3u new file mode 100644 index 000000000..7452e53aa --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/oce.m3u @@ -0,0 +1 @@ +#EXTM3U diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/sas.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/sas.m3u new file mode 100644 index 000000000..7452e53aa --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/sas.m3u @@ -0,0 +1 @@ +#EXTM3U diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/ssa.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/ssa.m3u new file mode 100644 index 000000000..7452e53aa --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/ssa.m3u @@ -0,0 +1 @@ +#EXTM3U diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/undefined.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/undefined.m3u new file mode 100644 index 000000000..eb0364cd5 --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/undefined.m3u @@ -0,0 +1,11 @@ +#EXTM3U +#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined" http-referrer="http://imn.iq" http-user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",Andorra TV (720p) [Not 24/7] +#EXTVLCOPT:http-referrer=http://imn.iq +#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 +http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index2.m3u8 +#EXTINF:-1 tvg-id="AndorraTV.ad@HD" tvg-logo="" group-title="Undefined",ATV HD +https://iptv-all.lanesh4d0w.repl.co/andorra/atv_hd +#EXTINF:-1 tvg-id="" tvg-logo="" group-title="Undefined",Daawah TV +http://51.15.246.58:8081/daawahtv/daawahtv2/playlist.m3u8 +#EXTINF:-1 tvg-id="Zoo.ad@HD" tvg-logo="" group-title="Undefined",Zoo (720p) +https://iptv-all.lanesh4d0w.repl.co/andorra/zoo diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/regions/wafr.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/regions/wafr.m3u new file mode 100644 index 000000000..7452e53aa --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/regions/wafr.m3u @@ -0,0 +1 @@ +#EXTM3U diff --git a/tests/__data__/expected/playlist_generate/.gh-pages/subdivisions/ca-on.m3u b/tests/__data__/expected/playlist_generate/.gh-pages/subdivisions/ca-on.m3u new file mode 100644 index 000000000..03a6963a3 --- /dev/null +++ b/tests/__data__/expected/playlist_generate/.gh-pages/subdivisions/ca-on.m3u @@ -0,0 +1,3 @@ +#EXTM3U +#EXTINF:-1 tvg-id="MeteoMedia.ca" tvg-logo="https://s1.twnmm.com/images/en_ca/mobile/logos/twn-mobile-logo.png" group-title="Weather",Meteomedia +http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8 diff --git a/tests/__data__/expected/playlist_update/cy.m3u b/tests/__data__/expected/playlist_update/cy.m3u index 987825ca1..13482713e 100644 --- a/tests/__data__/expected/playlist_update/cy.m3u +++ b/tests/__data__/expected/playlist_update/cy.m3u @@ -1,3 +1,3 @@ -#EXTM3U -#EXTINF:-1 tvg-id="AdaTV.cy",AdaTV -https://ythls.onrender.com/channel/UC40TUSUx490U5uR1lZt3Ajg.m3u8 +#EXTM3U +#EXTINF:-1 tvg-id="AdaTV.cy",AdaTV +https://ythls.onrender.com/channel/UC40TUSUx490U5uR1lZt3Ajg.m3u8 diff --git a/tests/__data__/expected/playlist_update/fr.m3u b/tests/__data__/expected/playlist_update/fr.m3u index 82856b338..f4f93afdc 100644 --- a/tests/__data__/expected/playlist_update/fr.m3u +++ b/tests/__data__/expected/playlist_update/fr.m3u @@ -1,5 +1,5 @@ -#EXTM3U -#EXTINF:-1 tvg-id="TFX.fr" http-referrer="https://pkpakiplay.xyz/" http-user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 17_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",TFX -#EXTVLCOPT:http-referrer=https://pkpakiplay.xyz/ -#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 17_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1 -https://stitcher-ipv4.pluto.tv/v1/stitch/embed/hls/channel/64c109a4798def0008a6e03e/master.m3u8?advertisingId={PSID}&appVersion=unknown&deviceDNT={TARGETOPT}&deviceId={PSID}&deviceLat=0&deviceLon=0&deviceMake=samsung&deviceModel=samsung&deviceType=samsung-tvplus&deviceVersion=unknown&embedPartner=samsung-tvplus&profileFloor=&profileLimit=&samsung_app_domain={APP_DOMAIN}&samsung_app_name={APP_NAME}&us_privacy=1YNY +#EXTM3U +#EXTINF:-1 tvg-id="TFX.fr" http-referrer="https://pkpakiplay.xyz/" http-user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 17_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1",TFX +#EXTVLCOPT:http-referrer=https://pkpakiplay.xyz/ +#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 17_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1 +https://stitcher-ipv4.pluto.tv/v1/stitch/embed/hls/channel/64c109a4798def0008a6e03e/master.m3u8?advertisingId={PSID}&appVersion=unknown&deviceDNT={TARGETOPT}&deviceId={PSID}&deviceLat=0&deviceLon=0&deviceMake=samsung&deviceModel=samsung&deviceType=samsung-tvplus&deviceVersion=unknown&embedPartner=samsung-tvplus&profileFloor=&profileLimit=&samsung_app_domain={APP_DOMAIN}&samsung_app_name={APP_NAME}&us_privacy=1YNY diff --git a/tests/__data__/expected/playlist_update/uk.m3u b/tests/__data__/expected/playlist_update/uk.m3u index 6ebfc71cd..a60ea6210 100644 --- a/tests/__data__/expected/playlist_update/uk.m3u +++ b/tests/__data__/expected/playlist_update/uk.m3u @@ -1,7 +1,7 @@ -#EXTM3U -#EXTINF:-1 tvg-id="BBCNews.uk",BBC News HD (720p) [Not 24/7] -http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8 -#EXTINF:-1 tvg-id="BBCNews.uk",BBC News HD (480p) [Geo-blocked] -http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/playlist.m3u8 -#EXTINF:-1 tvg-id="BeanoTV.uk",Beano TV -https://a5b4bacecd47433dad06d3189fc7422e.mediatailor.us-east-1.amazonaws.com/v1/manifest/04fd913bb278d8775298c26fdca9d9841f37601f/RakutenTV-eu_BeanoTV/b1f233d5-847c-437d-aa4f-f73e67a85323/2.m3u8 +#EXTM3U +#EXTINF:-1 tvg-id="BBCNews.uk",BBC News HD (720p) [Not 24/7] +http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8 +#EXTINF:-1 tvg-id="BBCNews.uk",BBC News HD (480p) [Geo-blocked] +http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/playlist.m3u8 +#EXTINF:-1 tvg-id="BeanoTV.uk",Beano TV +https://a5b4bacecd47433dad06d3189fc7422e.mediatailor.us-east-1.amazonaws.com/v1/manifest/04fd913bb278d8775298c26fdca9d9841f37601f/RakutenTV-eu_BeanoTV/b1f233d5-847c-437d-aa4f-f73e67a85323/2.m3u8 diff --git a/tests/__data__/expected/playlist_update/us.m3u b/tests/__data__/expected/playlist_update/us.m3u index 5271483e5..80ce51958 100644 --- a/tests/__data__/expected/playlist_update/us.m3u +++ b/tests/__data__/expected/playlist_update/us.m3u @@ -1,6 +1,6 @@ -#EXTM3U -#EXTINF:-1 tvg-id="BBCAmerica.us@East" http-user-agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36 Edge/12.246",BBC America East (720p) -#EXTVLCOPT:http-user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36 Edge/12.246 -https://servilive.com:3126/live/tele2000live.m3u8 -#EXTINF:-1 tvg-id="FastTV.us",Fast TV -https://3fa797d5.wurl.com/manifest/f36d25e7e52f1ba8d7e56eb859c636563214f541/T05PX01vdG9yVHJlbmRGYXN0VFZfSExT/b5e5e0e2-12b3-4312-93c9-c0a7c50b41ca/4.m3u8 +#EXTM3U +#EXTINF:-1 tvg-id="BBCAmerica.us@East" http-user-agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36 Edge/12.246",BBC America East (720p) +#EXTVLCOPT:http-user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36 Edge/12.246 +https://servilive.com:3126/live/tele2000live.m3u8 +#EXTINF:-1 tvg-id="FastTV.us",Fast TV +https://3fa797d5.wurl.com/manifest/f36d25e7e52f1ba8d7e56eb859c636563214f541/T05PX01vdG9yVHJlbmRGYXN0VFZfSExT/b5e5e0e2-12b3-4312-93c9-c0a7c50b41ca/4.m3u8 diff --git a/tests/__data__/expected/readme_update/_readme.md b/tests/__data__/expected/readme_update/_readme.md index 9fc78acc2..67b094c3d 100644 --- a/tests/__data__/expected/readme_update/_readme.md +++ b/tests/__data__/expected/readme_update/_readme.md @@ -1,257 +1,257 @@ -# IPTV [![update](https://github.com/iptv-org/iptv/actions/workflows/update.yml/badge.svg)](https://github.com/iptv-org/iptv/actions/workflows/update.yml) - -Collection of publicly available IPTV (Internet Protocol television) channels from all over the world. - -## Table of contents - -- 🚀 [How to use?](#how-to-use) -- 📺 [Playlists](#playlists) -- 🗄 [Database](#database) -- 👨‍💻 [API](#api) -- 📚 [Resources](#resources) -- 💬 [Discussions](#discussions) -- ❓ [FAQ](#faq) -- 🛠 [Contribution](#contribution) -- ⚖ [Legal](#legal) -- © [License](#license) - -## How to use? - -Simply insert one of the links below into [any video player](https://github.com/iptv-org/awesome-iptv#apps) that supports live streaming and press _Open_. - -![VLC Network Panel](https://github.com/iptv-org/iptv/raw/master/.readme/preview.png) - -## Playlists - -There are several versions of playlists that differ in the way they are grouped. - -### Main playlist - -Playlist includes all known channels except adult channels. - -``` -https://iptv-org.github.io/iptv/index.m3u -``` - -And here is the full version: - -``` -https://iptv-org.github.io/iptv/index.nsfw.m3u -``` - -### Grouped by category - -
-Expand -
- -Playlist in which each channel has its _category_ as a group title: - -``` -https://iptv-org.github.io/iptv/index.category.m3u -``` - -Same thing, but split up into separate files: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
CategoryChannelsPlaylist
Animation0https://iptv-org.github.io/iptv/categories/animation.m3u
Auto0https://iptv-org.github.io/iptv/categories/auto.m3u
Business0https://iptv-org.github.io/iptv/categories/business.m3u
Classic0https://iptv-org.github.io/iptv/categories/classic.m3u
Comedy0https://iptv-org.github.io/iptv/categories/comedy.m3u
Cooking0https://iptv-org.github.io/iptv/categories/cooking.m3u
Culture0https://iptv-org.github.io/iptv/categories/culture.m3u
Documentary0https://iptv-org.github.io/iptv/categories/documentary.m3u
Education0https://iptv-org.github.io/iptv/categories/education.m3u
Entertainment0https://iptv-org.github.io/iptv/categories/entertainment.m3u
Family0https://iptv-org.github.io/iptv/categories/family.m3u
General2https://iptv-org.github.io/iptv/categories/general.m3u
Kids0https://iptv-org.github.io/iptv/categories/kids.m3u
Legislative0https://iptv-org.github.io/iptv/categories/legislative.m3u
Lifestyle0https://iptv-org.github.io/iptv/categories/lifestyle.m3u
Movies0https://iptv-org.github.io/iptv/categories/movies.m3u
Music0https://iptv-org.github.io/iptv/categories/music.m3u
News1https://iptv-org.github.io/iptv/categories/news.m3u
Outdoor0https://iptv-org.github.io/iptv/categories/outdoor.m3u
Relax0https://iptv-org.github.io/iptv/categories/relax.m3u
Religious0https://iptv-org.github.io/iptv/categories/religious.m3u
Science0https://iptv-org.github.io/iptv/categories/science.m3u
Series0https://iptv-org.github.io/iptv/categories/series.m3u
Shop0https://iptv-org.github.io/iptv/categories/shop.m3u
Sports0https://iptv-org.github.io/iptv/categories/sports.m3u
Travel0https://iptv-org.github.io/iptv/categories/travel.m3u
Weather1https://iptv-org.github.io/iptv/categories/weather.m3u
XXX1https://iptv-org.github.io/iptv/categories/xxx.m3u
Undefined3https://iptv-org.github.io/iptv/categories/undefined.m3u
- -
- -### Grouped by language - -
-Expand -
- -Playlist in which each channel has its _language_ as a group title: - -``` -https://iptv-org.github.io/iptv/index.language.m3u -``` - -Same thing, but split up into separate files: - - - - - - - - - - - - - -
LanguageChannelsPlaylist
Catalan1https://iptv-org.github.io/iptv/languages/cat.m3u
English1https://iptv-org.github.io/iptv/languages/eng.m3u
French1https://iptv-org.github.io/iptv/languages/fra.m3u
Russian1https://iptv-org.github.io/iptv/languages/rus.m3u
Undefined2https://iptv-org.github.io/iptv/languages/undefined.m3u
- -
- -### Grouped by country - -
-Expand -
- -Playlist in which each channel has its _country_ as a group title: - -``` -https://iptv-org.github.io/iptv/index.country.m3u -``` - -Same thing, but split up into separate files: - - - - - - - - - - - - - - - - - - - - - -
CountryChannelsPlaylist
🇨🇲 Cameroon1https://iptv-org.github.io/iptv/countries/cm.m3u
🇨🇦 Canada2https://iptv-org.github.io/iptv/countries/ca.m3u
      Ontario1https://iptv-org.github.io/iptv/subdivisions/ca-on.m3u
🇨🇻 Cape Verde1https://iptv-org.github.io/iptv/countries/cv.m3u
🇨🇬 Republic of the Congo1https://iptv-org.github.io/iptv/countries/cg.m3u
🇷🇪 Réunion1https://iptv-org.github.io/iptv/countries/re.m3u
🇷🇴 Romania1https://iptv-org.github.io/iptv/countries/ro.m3u
🇷🇺 Russia2https://iptv-org.github.io/iptv/countries/ru.m3u
🇷🇼 Rwanda1https://iptv-org.github.io/iptv/countries/rw.m3u
🇧🇱 Saint Barthélemy1https://iptv-org.github.io/iptv/countries/bl.m3u
🇸🇭 Saint Helena1https://iptv-org.github.io/iptv/countries/sh.m3u
🇰🇳 Saint Kitts and Nevis1https://iptv-org.github.io/iptv/countries/kn.m3u
Undefined2https://iptv-org.github.io/iptv/countries/undefined.m3u
- -
- -### Grouped by region - -
-Expand -
- -Playlist in which each channel has its _region_ as a group title: - -``` -https://iptv-org.github.io/iptv/index.region.m3u -``` - -Same thing, but split up into separate files: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
RegionChannelsPlaylist
Africa0https://iptv-org.github.io/iptv/regions/afr.m3u
Americas1https://iptv-org.github.io/iptv/regions/amer.m3u
Arab world0https://iptv-org.github.io/iptv/regions/arab.m3u
Asia2https://iptv-org.github.io/iptv/regions/asia.m3u
Asia-Pacific1https://iptv-org.github.io/iptv/regions/apac.m3u
Association of Southeast Asian Nations0https://iptv-org.github.io/iptv/regions/asean.m3u
Caribbean0https://iptv-org.github.io/iptv/regions/carib.m3u
Central America0https://iptv-org.github.io/iptv/regions/cenamer.m3u
Central Asia0https://iptv-org.github.io/iptv/regions/cas.m3u
Commonwealth of Independent States1https://iptv-org.github.io/iptv/regions/cis.m3u
Europe3https://iptv-org.github.io/iptv/regions/eur.m3u
Europe, the Middle East and Africa3https://iptv-org.github.io/iptv/regions/emea.m3u
Hispanic America0https://iptv-org.github.io/iptv/regions/hispam.m3u
Latin America0https://iptv-org.github.io/iptv/regions/latam.m3u
Latin America and the Caribbean0https://iptv-org.github.io/iptv/regions/lac.m3u
Maghreb0https://iptv-org.github.io/iptv/regions/maghreb.m3u
Middle East0https://iptv-org.github.io/iptv/regions/mideast.m3u
Middle East and North Africa0https://iptv-org.github.io/iptv/regions/mena.m3u
Nordics0https://iptv-org.github.io/iptv/regions/nord.m3u
North America1https://iptv-org.github.io/iptv/regions/noram.m3u
Northern America1https://iptv-org.github.io/iptv/regions/nam.m3u
Oceania0https://iptv-org.github.io/iptv/regions/oce.m3u
South America0https://iptv-org.github.io/iptv/regions/southam.m3u
South Asia1https://iptv-org.github.io/iptv/regions/sas.m3u
Sub-Saharan Africa0https://iptv-org.github.io/iptv/regions/ssa.m3u
West Africa0https://iptv-org.github.io/iptv/regions/wafr.m3u
Worldwide1https://iptv-org.github.io/iptv/regions/int.m3u
Undefined2https://iptv-org.github.io/iptv/regions/undefined.m3u
- -
- -## Database - -All channel data is taken from the [iptv-org/database](https://github.com/iptv-org/database) repository. If you find any errors please open a new [issue](https://github.com/iptv-org/database/issues) there. - -## API - -The API documentation can be found in the [iptv-org/api](https://github.com/iptv-org/api) repository. - -## Resources - -Links to other useful IPTV-related resources can be found in the [iptv-org/awesome-iptv](https://github.com/iptv-org/awesome-iptv) repository. - -## Discussions - -If you need help finding a channel, have a question or idea, welcome to the [Discussions](https://github.com/orgs/iptv-org/discussions). - -## FAQ - -The answers to the most popular questions can be found in the [FAQ.md](FAQ.md) file. - -## Contribution - -Please make sure to read the [Contributing Guide](CONTRIBUTING.md) before sending an issue or making a pull request. - -And thank you to everyone who has already contributed! - -### Backers - - - -### Contributors - - - -## Legal - -No video files are stored in this repository. The repository simply contains user-submitted links to publicly available video stream URLs, which to the best of our knowledge have been intentionally made publicly by the copyright holders. If any links in these playlists infringe on your rights as a copyright holder, they may be removed by sending a [pull request](https://github.com/iptv-org/iptv/pulls) or opening an [issue](https://github.com/iptv-org/iptv/issues/new?assignees=freearhey&labels=removal+request&template=--removal-request.yml&title=Remove%3A+). However, note that we have **no control** over the destination of the link, and just removing the link from the playlist will not remove its contents from the web. Note that linking does not directly infringe copyright because no copy is made on the site providing the link, and thus this is **not** a valid reason to send a DMCA notice to GitHub. To remove this content from the web, you should contact the web host that's actually hosting the content (**not** GitHub, nor the maintainers of this repository). - -## License - -[![CC0](http://mirrors.creativecommons.org/presskit/buttons/88x31/svg/cc-zero.svg)](LICENSE) +# IPTV [![update](https://github.com/iptv-org/iptv/actions/workflows/update.yml/badge.svg)](https://github.com/iptv-org/iptv/actions/workflows/update.yml) + +Collection of publicly available IPTV (Internet Protocol television) channels from all over the world. + +## Table of contents + +- 🚀 [How to use?](#how-to-use) +- 📺 [Playlists](#playlists) +- 🗄 [Database](#database) +- 👨‍💻 [API](#api) +- 📚 [Resources](#resources) +- 💬 [Discussions](#discussions) +- ❓ [FAQ](#faq) +- 🛠 [Contribution](#contribution) +- ⚖ [Legal](#legal) +- © [License](#license) + +## How to use? + +Simply insert one of the links below into [any video player](https://github.com/iptv-org/awesome-iptv#apps) that supports live streaming and press _Open_. + +![VLC Network Panel](https://github.com/iptv-org/iptv/raw/master/.readme/preview.png) + +## Playlists + +There are several versions of playlists that differ in the way they are grouped. + +### Main playlist + +Playlist includes all known channels except adult channels. + +``` +https://iptv-org.github.io/iptv/index.m3u +``` + +And here is the full version: + +``` +https://iptv-org.github.io/iptv/index.nsfw.m3u +``` + +### Grouped by category + +
+Expand +
+ +Playlist in which each channel has its _category_ as a group title: + +``` +https://iptv-org.github.io/iptv/index.category.m3u +``` + +Same thing, but split up into separate files: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CategoryChannelsPlaylist
Animation0https://iptv-org.github.io/iptv/categories/animation.m3u
Auto0https://iptv-org.github.io/iptv/categories/auto.m3u
Business0https://iptv-org.github.io/iptv/categories/business.m3u
Classic0https://iptv-org.github.io/iptv/categories/classic.m3u
Comedy0https://iptv-org.github.io/iptv/categories/comedy.m3u
Cooking0https://iptv-org.github.io/iptv/categories/cooking.m3u
Culture0https://iptv-org.github.io/iptv/categories/culture.m3u
Documentary0https://iptv-org.github.io/iptv/categories/documentary.m3u
Education0https://iptv-org.github.io/iptv/categories/education.m3u
Entertainment0https://iptv-org.github.io/iptv/categories/entertainment.m3u
Family0https://iptv-org.github.io/iptv/categories/family.m3u
General2https://iptv-org.github.io/iptv/categories/general.m3u
Kids0https://iptv-org.github.io/iptv/categories/kids.m3u
Legislative0https://iptv-org.github.io/iptv/categories/legislative.m3u
Lifestyle0https://iptv-org.github.io/iptv/categories/lifestyle.m3u
Movies0https://iptv-org.github.io/iptv/categories/movies.m3u
Music0https://iptv-org.github.io/iptv/categories/music.m3u
News1https://iptv-org.github.io/iptv/categories/news.m3u
Outdoor0https://iptv-org.github.io/iptv/categories/outdoor.m3u
Relax0https://iptv-org.github.io/iptv/categories/relax.m3u
Religious0https://iptv-org.github.io/iptv/categories/religious.m3u
Science0https://iptv-org.github.io/iptv/categories/science.m3u
Series0https://iptv-org.github.io/iptv/categories/series.m3u
Shop0https://iptv-org.github.io/iptv/categories/shop.m3u
Sports0https://iptv-org.github.io/iptv/categories/sports.m3u
Travel0https://iptv-org.github.io/iptv/categories/travel.m3u
Weather1https://iptv-org.github.io/iptv/categories/weather.m3u
XXX1https://iptv-org.github.io/iptv/categories/xxx.m3u
Undefined3https://iptv-org.github.io/iptv/categories/undefined.m3u
+ +
+ +### Grouped by language + +
+Expand +
+ +Playlist in which each channel has its _language_ as a group title: + +``` +https://iptv-org.github.io/iptv/index.language.m3u +``` + +Same thing, but split up into separate files: + + + + + + + + + + + + + +
LanguageChannelsPlaylist
Catalan1https://iptv-org.github.io/iptv/languages/cat.m3u
English1https://iptv-org.github.io/iptv/languages/eng.m3u
French1https://iptv-org.github.io/iptv/languages/fra.m3u
Russian1https://iptv-org.github.io/iptv/languages/rus.m3u
Undefined2https://iptv-org.github.io/iptv/languages/undefined.m3u
+ +
+ +### Grouped by country + +
+Expand +
+ +Playlist in which each channel has its _country_ as a group title: + +``` +https://iptv-org.github.io/iptv/index.country.m3u +``` + +Same thing, but split up into separate files: + + + + + + + + + + + + + + + + + + + + + +
CountryChannelsPlaylist
🇨🇲 Cameroon1https://iptv-org.github.io/iptv/countries/cm.m3u
🇨🇦 Canada2https://iptv-org.github.io/iptv/countries/ca.m3u
      Ontario1https://iptv-org.github.io/iptv/subdivisions/ca-on.m3u
🇨🇻 Cape Verde1https://iptv-org.github.io/iptv/countries/cv.m3u
🇨🇬 Republic of the Congo1https://iptv-org.github.io/iptv/countries/cg.m3u
🇷🇪 Réunion1https://iptv-org.github.io/iptv/countries/re.m3u
🇷🇴 Romania1https://iptv-org.github.io/iptv/countries/ro.m3u
🇷🇺 Russia2https://iptv-org.github.io/iptv/countries/ru.m3u
🇷🇼 Rwanda1https://iptv-org.github.io/iptv/countries/rw.m3u
🇧🇱 Saint Barthélemy1https://iptv-org.github.io/iptv/countries/bl.m3u
🇸🇭 Saint Helena1https://iptv-org.github.io/iptv/countries/sh.m3u
🇰🇳 Saint Kitts and Nevis1https://iptv-org.github.io/iptv/countries/kn.m3u
Undefined2https://iptv-org.github.io/iptv/countries/undefined.m3u
+ +
+ +### Grouped by region + +
+Expand +
+ +Playlist in which each channel has its _region_ as a group title: + +``` +https://iptv-org.github.io/iptv/index.region.m3u +``` + +Same thing, but split up into separate files: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
RegionChannelsPlaylist
Africa0https://iptv-org.github.io/iptv/regions/afr.m3u
Americas1https://iptv-org.github.io/iptv/regions/amer.m3u
Arab world0https://iptv-org.github.io/iptv/regions/arab.m3u
Asia2https://iptv-org.github.io/iptv/regions/asia.m3u
Asia-Pacific1https://iptv-org.github.io/iptv/regions/apac.m3u
Association of Southeast Asian Nations0https://iptv-org.github.io/iptv/regions/asean.m3u
Caribbean0https://iptv-org.github.io/iptv/regions/carib.m3u
Central America0https://iptv-org.github.io/iptv/regions/cenamer.m3u
Central Asia0https://iptv-org.github.io/iptv/regions/cas.m3u
Commonwealth of Independent States1https://iptv-org.github.io/iptv/regions/cis.m3u
Europe3https://iptv-org.github.io/iptv/regions/eur.m3u
Europe, the Middle East and Africa3https://iptv-org.github.io/iptv/regions/emea.m3u
Hispanic America0https://iptv-org.github.io/iptv/regions/hispam.m3u
Latin America0https://iptv-org.github.io/iptv/regions/latam.m3u
Latin America and the Caribbean0https://iptv-org.github.io/iptv/regions/lac.m3u
Maghreb0https://iptv-org.github.io/iptv/regions/maghreb.m3u
Middle East0https://iptv-org.github.io/iptv/regions/mideast.m3u
Middle East and North Africa0https://iptv-org.github.io/iptv/regions/mena.m3u
Nordics0https://iptv-org.github.io/iptv/regions/nord.m3u
North America1https://iptv-org.github.io/iptv/regions/noram.m3u
Northern America1https://iptv-org.github.io/iptv/regions/nam.m3u
Oceania0https://iptv-org.github.io/iptv/regions/oce.m3u
South America0https://iptv-org.github.io/iptv/regions/southam.m3u
South Asia1https://iptv-org.github.io/iptv/regions/sas.m3u
Sub-Saharan Africa0https://iptv-org.github.io/iptv/regions/ssa.m3u
West Africa0https://iptv-org.github.io/iptv/regions/wafr.m3u
Worldwide1https://iptv-org.github.io/iptv/regions/int.m3u
Undefined2https://iptv-org.github.io/iptv/regions/undefined.m3u
+ +
+ +## Database + +All channel data is taken from the [iptv-org/database](https://github.com/iptv-org/database) repository. If you find any errors please open a new [issue](https://github.com/iptv-org/database/issues) there. + +## API + +The API documentation can be found in the [iptv-org/api](https://github.com/iptv-org/api) repository. + +## Resources + +Links to other useful IPTV-related resources can be found in the [iptv-org/awesome-iptv](https://github.com/iptv-org/awesome-iptv) repository. + +## Discussions + +If you need help finding a channel, have a question or idea, welcome to the [Discussions](https://github.com/orgs/iptv-org/discussions). + +## FAQ + +The answers to the most popular questions can be found in the [FAQ.md](FAQ.md) file. + +## Contribution + +Please make sure to read the [Contributing Guide](CONTRIBUTING.md) before sending an issue or making a pull request. + +And thank you to everyone who has already contributed! + +### Backers + + + +### Contributors + + + +## Legal + +No video files are stored in this repository. The repository simply contains user-submitted links to publicly available video stream URLs, which to the best of our knowledge have been intentionally made publicly by the copyright holders. If any links in these playlists infringe on your rights as a copyright holder, they may be removed by sending a [pull request](https://github.com/iptv-org/iptv/pulls) or opening an [issue](https://github.com/iptv-org/iptv/issues/new?assignees=freearhey&labels=removal+request&template=--removal-request.yml&title=Remove%3A+). However, note that we have **no control** over the destination of the link, and just removing the link from the playlist will not remove its contents from the web. Note that linking does not directly infringe copyright because no copy is made on the site providing the link, and thus this is **not** a valid reason to send a DMCA notice to GitHub. To remove this content from the web, you should contact the web host that's actually hosting the content (**not** GitHub, nor the maintainers of this repository). + +## License + +[![CC0](http://mirrors.creativecommons.org/presskit/buttons/88x31/svg/cc-zero.svg)](LICENSE) diff --git a/tests/__data__/input/api_generate/ad.m3u b/tests/__data__/input/api_generate/ad.m3u index 64a1dc0cf..9d8926e0e 100644 --- a/tests/__data__/input/api_generate/ad.m3u +++ b/tests/__data__/input/api_generate/ad.m3u @@ -1,5 +1,5 @@ -#EXTM3U -#EXTINF:-1 tvg-id="Zoo.ad",Zoo (720p) -https://iptv-all.lanesh4d0w.repl.co/andorra/zoo -#EXTINF:-1 tvg-id="AndorraTV.ad@SD",ATV -https://iptv-all.lanesh4d0w.repl.co/andorra/atv +#EXTM3U +#EXTINF:-1 tvg-id="Zoo.ad",Zoo (720p) +https://iptv-all.lanesh4d0w.repl.co/andorra/zoo +#EXTINF:-1 tvg-id="AndorraTV.ad@SD",ATV +https://iptv-all.lanesh4d0w.repl.co/andorra/atv diff --git a/tests/__data__/input/api_generate/ca.m3u b/tests/__data__/input/api_generate/ca.m3u index 9c198b682..672601d7f 100644 --- a/tests/__data__/input/api_generate/ca.m3u +++ b/tests/__data__/input/api_generate/ca.m3u @@ -1,3 +1,3 @@ -#EXTM3U -#EXTINF:-1 tvg-id="MeteoMedia.ca",Meteomedia -http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8 +#EXTM3U +#EXTINF:-1 tvg-id="MeteoMedia.ca",Meteomedia +http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8 diff --git a/tests/__data__/input/api_generate/in.m3u b/tests/__data__/input/api_generate/in.m3u index 62594eb4b..c375e17ac 100644 --- a/tests/__data__/input/api_generate/in.m3u +++ b/tests/__data__/input/api_generate/in.m3u @@ -1,3 +1,3 @@ -#EXTM3U -#EXTINF:-1 tvg-id="",Daawah TV -http://51.15.246.58:8081/daawahtv/daawahtv2/playlist.m3u8 +#EXTM3U +#EXTINF:-1 tvg-id="",Daawah TV +http://51.15.246.58:8081/daawahtv/daawahtv2/playlist.m3u8 diff --git a/tests/__data__/input/api_generate/uk.m3u b/tests/__data__/input/api_generate/uk.m3u index 5e8c7318e..4a8a1f64a 100644 --- a/tests/__data__/input/api_generate/uk.m3u +++ b/tests/__data__/input/api_generate/uk.m3u @@ -1,3 +1,3 @@ -#EXTM3U -#EXTINF:-1 tvg-id="BBCNews.uk",BBC News HD -http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8 +#EXTM3U +#EXTINF:-1 tvg-id="BBCNews.uk",BBC News HD +http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8 diff --git a/tests/__data__/input/api_generate/unsorted.m3u b/tests/__data__/input/api_generate/unsorted.m3u index 1a9c40b9f..96f9299e2 100644 --- a/tests/__data__/input/api_generate/unsorted.m3u +++ b/tests/__data__/input/api_generate/unsorted.m3u @@ -1,9 +1,9 @@ -#EXTM3U -#EXTINF:-1 tvg-id="LDPRTV.ru",ЛДПР ТВ (1080p) -http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8 -#EXTINF:-1 tvg-id="VisitXTV.nl",Visit-X TV -https://stream.visit-x.tv/vxtv/ngrp:live_all/30fps.m3u8 -#EXTINF:-1 tvg-id="" user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",Andorra TV (720p) [Not 24/7] -#EXTVLCOPT:http-referrer=http://imn.iq -#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 -http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index2.m3u8 +#EXTM3U +#EXTINF:-1 tvg-id="LDPRTV.ru",ЛДПР ТВ (1080p) +http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8 +#EXTINF:-1 tvg-id="VisitXTV.nl",Visit-X TV +https://stream.visit-x.tv/vxtv/ngrp:live_all/30fps.m3u8 +#EXTINF:-1 tvg-id="" user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",Andorra TV (720p) [Not 24/7] +#EXTVLCOPT:http-referrer=http://imn.iq +#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 +http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index2.m3u8 diff --git a/tests/__data__/input/data/guides.json b/tests/__data__/input/data/guides.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/tests/__data__/input/data/guides.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/__data__/input/data/streams.json b/tests/__data__/input/data/streams.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/tests/__data__/input/data/streams.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/tests/__data__/input/playlist_edit/playlist.m3u b/tests/__data__/input/playlist_edit/playlist.m3u new file mode 100644 index 000000000..0abd41b6f --- /dev/null +++ b/tests/__data__/input/playlist_edit/playlist.m3u @@ -0,0 +1,5 @@ +#EXTM3U +#EXTINF:-1 tvg-id="HewadTV.af",Hewad TV (720p) [Not 24/7] +http://51.210.199.58/hls/stream.m3u8 +#EXTINF:-1 tvg-id="",Télévision française 1 (480p) +https://live.relentlessinnovations.net:1936/imantv/imantv/playlist.m3u8 \ No newline at end of file diff --git a/tests/__data__/input/playlist_format/in.m3u b/tests/__data__/input/playlist_format/in.m3u index 47c77da2e..3d0bcd359 100644 --- a/tests/__data__/input/playlist_format/in.m3u +++ b/tests/__data__/input/playlist_format/in.m3u @@ -1,3 +1,3 @@ -#EXTM3U -#EXTINF:-1 tvg-id="mn.in",Manorama News -2 [U3] (480p) [Geo-blocked] [Not 24/7] -https://ythls.onrender.com/channel/UCP0uG-mcMImgKnJz-VjJZmQ.m3u8 +#EXTM3U +#EXTINF:-1 tvg-id="mn.in",Manorama News -2 [U3] (480p) [Geo-blocked] [Not 24/7] +https://ythls.onrender.com/channel/UCP0uG-mcMImgKnJz-VjJZmQ.m3u8 diff --git a/tests/__data__/input/playlist_format/nl.m3u b/tests/__data__/input/playlist_format/nl.m3u index 5fdeaea52..39d56a3ef 100644 --- a/tests/__data__/input/playlist_format/nl.m3u +++ b/tests/__data__/input/playlist_format/nl.m3u @@ -1,13 +1,13 @@ -#EXTM3U -#EXTINF:-1 tvg-id="NPO2.nl",NPO 2 (302p) [Geo-blocked] -#EXTVLCOPT:http-referrer=http://imn.iq -#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 -http://stream.tvtap.net:8081/live/nl-npo2.stream/playlist.m3u8? -#EXTINF:-1 tvg-id="NPO2.nl",NPO 2 [Geo-blocked] -http://resolver.streaming.api.nos.nl/livestream?url=/live/npo/tvlive/npo2/npo22.isml/.m3u8 -#EXTINF:-1 tvg-id="NPO2.nl",NPO 2 (342p) -http://resolver.streaming.api.nos.nl/livestream?url=/live/npo/tvlive/npo2/npo2.isml/.m3u8 -#EXTINF:-1 tvg-id="NPO1.nl@SD",NPO 1 (342p) [Geo-blocked] -http://resolver.streaming.api.nos.nl/livestream?url=/live/npo/tvlive/npo1/npo1.isml/.m3u8 -#EXTINF:-1 tvg-id="",NPO 2 (Duplicate) -http://resolver.streaming.api.nos.nl/livestream?url=/live/npo/tvlive/npo2/npo2.isml/.m3u8 +#EXTM3U +#EXTINF:-1 tvg-id="NPO2.nl",NPO 2 (302p) [Geo-blocked] +#EXTVLCOPT:http-referrer=http://imn.iq +#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 +http://stream.tvtap.net:8081/live/nl-npo2.stream/playlist.m3u8? +#EXTINF:-1 tvg-id="NPO2.nl",NPO 2 [Geo-blocked] +http://resolver.streaming.api.nos.nl/livestream?url=/live/npo/tvlive/npo2/npo22.isml/.m3u8 +#EXTINF:-1 tvg-id="NPO2.nl",NPO 2 (342p) +http://resolver.streaming.api.nos.nl/livestream?url=/live/npo/tvlive/npo2/npo2.isml/.m3u8 +#EXTINF:-1 tvg-id="NPO1.nl@SD",NPO 1 (342p) [Geo-blocked] +http://resolver.streaming.api.nos.nl/livestream?url=/live/npo/tvlive/npo1/npo1.isml/.m3u8 +#EXTINF:-1 tvg-id="",NPO 2 (Duplicate) +http://resolver.streaming.api.nos.nl/livestream?url=/live/npo/tvlive/npo2/npo2.isml/.m3u8 diff --git a/tests/__data__/input/playlist_generate/ad.m3u b/tests/__data__/input/playlist_generate/ad.m3u index 9a1c9e175..3c3bcae1d 100644 --- a/tests/__data__/input/playlist_generate/ad.m3u +++ b/tests/__data__/input/playlist_generate/ad.m3u @@ -1,9 +1,9 @@ -#EXTM3U -#EXTINF:-1 tvg-id="Zoo.ad@HD",Zoo (720p) -https://iptv-all.lanesh4d0w.repl.co/andorra/zoo -#EXTINF:-1 tvg-id="AndorraTV.ad@SD",ATV -https://iptv-all.lanesh4d0w.repl.co/andorra/atv -#EXTINF:-1 tvg-id="AndorraTV.ad@HD",ATV HD -https://iptv-all.lanesh4d0w.repl.co/andorra/atv_hd -#EXTINF:-1 tvg-id="AndorraTV.ad",ATV -https://iptv-all.lanesh4d0w.repl.co/andorra/atv2 +#EXTM3U +#EXTINF:-1 tvg-id="Zoo.ad@HD",Zoo (720p) +https://iptv-all.lanesh4d0w.repl.co/andorra/zoo +#EXTINF:-1 tvg-id="AndorraTV.ad@SD",ATV +https://iptv-all.lanesh4d0w.repl.co/andorra/atv +#EXTINF:-1 tvg-id="AndorraTV.ad@HD",ATV HD +https://iptv-all.lanesh4d0w.repl.co/andorra/atv_hd +#EXTINF:-1 tvg-id="AndorraTV.ad",ATV +https://iptv-all.lanesh4d0w.repl.co/andorra/atv2 diff --git a/tests/__data__/input/playlist_generate/ca.m3u b/tests/__data__/input/playlist_generate/ca.m3u index 9c198b682..672601d7f 100644 --- a/tests/__data__/input/playlist_generate/ca.m3u +++ b/tests/__data__/input/playlist_generate/ca.m3u @@ -1,3 +1,3 @@ -#EXTM3U -#EXTINF:-1 tvg-id="MeteoMedia.ca",Meteomedia -http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8 +#EXTM3U +#EXTINF:-1 tvg-id="MeteoMedia.ca",Meteomedia +http://encodercdn1.frontline.ca/encoder181/output/Meteo_Media_720p/playlist.m3u8 diff --git a/tests/__data__/input/playlist_generate/in.m3u b/tests/__data__/input/playlist_generate/in.m3u index 62594eb4b..c375e17ac 100644 --- a/tests/__data__/input/playlist_generate/in.m3u +++ b/tests/__data__/input/playlist_generate/in.m3u @@ -1,3 +1,3 @@ -#EXTM3U -#EXTINF:-1 tvg-id="",Daawah TV -http://51.15.246.58:8081/daawahtv/daawahtv2/playlist.m3u8 +#EXTM3U +#EXTINF:-1 tvg-id="",Daawah TV +http://51.15.246.58:8081/daawahtv/daawahtv2/playlist.m3u8 diff --git a/tests/__data__/input/playlist_generate/kg.m3u b/tests/__data__/input/playlist_generate/kg.m3u index 9c1d5a575..9bf0832b5 100644 --- a/tests/__data__/input/playlist_generate/kg.m3u +++ b/tests/__data__/input/playlist_generate/kg.m3u @@ -1,3 +1,3 @@ -#EXTM3U -#EXTINF:-1 tvg-id="ElTR.kg",ЭлТР (480p) [Not 24/7] -http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8 +#EXTM3U +#EXTINF:-1 tvg-id="ElTR.kg",ЭлТР (480p) [Not 24/7] +http://gohoski.fvds.ru:3000/mediabay/162/index.m3u8 diff --git a/tests/__data__/input/playlist_generate/uk.m3u b/tests/__data__/input/playlist_generate/uk.m3u index 5e8c7318e..4a8a1f64a 100644 --- a/tests/__data__/input/playlist_generate/uk.m3u +++ b/tests/__data__/input/playlist_generate/uk.m3u @@ -1,3 +1,3 @@ -#EXTM3U -#EXTINF:-1 tvg-id="BBCNews.uk",BBC News HD -http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8 +#EXTM3U +#EXTINF:-1 tvg-id="BBCNews.uk",BBC News HD +http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8 diff --git a/tests/__data__/input/playlist_generate/unsorted.m3u b/tests/__data__/input/playlist_generate/unsorted.m3u index b2b798b15..826414ad2 100644 --- a/tests/__data__/input/playlist_generate/unsorted.m3u +++ b/tests/__data__/input/playlist_generate/unsorted.m3u @@ -1,11 +1,11 @@ -#EXTM3U -#EXTINF:-1 tvg-id="LDPRTV.ru",ЛДПР ТВ (1080p) -http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8 -#EXTINF:-1 tvg-id="VisitXTV.nl",Visit-X TV -https://stream.visit-x.tv/vxtv/ngrp:live_all/30fps.m3u8 -#EXTINF:-1 tvg-id="" user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",Andorra TV (720p) [Not 24/7] -#EXTVLCOPT:http-referrer=http://imn.iq -#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 -http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index2.m3u8 -#EXTINF:-1 tvg-id="DunaWorld.hu",Duna World (576i) -http://146.59.85.40:89/dunaworld/index.m3u8 +#EXTM3U +#EXTINF:-1 tvg-id="LDPRTV.ru",ЛДПР ТВ (1080p) +http://46.46.143.222:1935/live/mp4:ldpr.stream/blocked.m3u8 +#EXTINF:-1 tvg-id="VisitXTV.nl",Visit-X TV +https://stream.visit-x.tv/vxtv/ngrp:live_all/30fps.m3u8 +#EXTINF:-1 tvg-id="" user-agent="Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",Andorra TV (720p) [Not 24/7] +#EXTVLCOPT:http-referrer=http://imn.iq +#EXTVLCOPT:http-user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 +http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index2.m3u8 +#EXTINF:-1 tvg-id="DunaWorld.hu",Duna World (576i) +http://146.59.85.40:89/dunaworld/index.m3u8 diff --git a/tests/__data__/input/playlist_test/ag.m3u b/tests/__data__/input/playlist_test/ag.m3u index 66b32b21d..f4716e013 100644 --- a/tests/__data__/input/playlist_test/ag.m3u +++ b/tests/__data__/input/playlist_test/ag.m3u @@ -1,5 +1,5 @@ -#EXTM3U -#EXTINF:-1 tvg-id="ABSTV.ag",ABS TV -https://tego-cdn2a.sibercdn.com/Live_TV-ABSTV-10/tracks-v3a1/rewind-7200.m3u8?token=e5f61e7be8363eb781b4bdfe591bf917dd529c1a-SjY3NzRTbDZQNnFQVkZaNkZja2RxV3JKc1VBa05zQkdMNStJakRGV0VTTzNrOEVGVUlIQmxta1NLV0o3bzdVdQ-1736094545-1736008145 -#EXTINF:-1 tvg-id="ABSTV.ag@HD",ABS TV (1080p) [Not 24/7] +#EXTM3U +#EXTINF:-1 tvg-id="ABSTV.ag",ABS TV +https://tego-cdn2a.sibercdn.com/Live_TV-ABSTV-10/tracks-v3a1/rewind-7200.m3u8?token=e5f61e7be8363eb781b4bdfe591bf917dd529c1a-SjY3NzRTbDZQNnFQVkZaNkZja2RxV3JKc1VBa05zQkdMNStJakRGV0VTTzNrOEVGVUlIQmxta1NLV0o3bzdVdQ-1736094545-1736008145 +#EXTINF:-1 tvg-id="ABSTV.ag@HD",ABS TV (1080p) [Not 24/7] https://query-streamlink.herokuapp.com/iptv-query?streaming-ip=https://www.twitch.tv/absliveantigua3 \ No newline at end of file diff --git a/tests/__data__/input/playlist_update/br.m3u b/tests/__data__/input/playlist_update/br.m3u index 6dcdaee15..31a869861 100644 --- a/tests/__data__/input/playlist_update/br.m3u +++ b/tests/__data__/input/playlist_update/br.m3u @@ -1,6 +1,6 @@ -#EXTM3U -#EXTINF:-1 tvg-id="",VTV [Not 24/7] -https://ythls.onrender.com/channel/UC40TUSUx490U5uR1lZt3Ajg.m3u8 -#EXTINF:-1 tvg-id="",Tele2000 [Not 24/7] -#EXTVLCOPT:http-referrer=https://example2.com/ -https://servilive.com:3126/live/tele2000live.m3u8 +#EXTM3U +#EXTINF:-1 tvg-id="",VTV [Not 24/7] +https://ythls.onrender.com/channel/UC40TUSUx490U5uR1lZt3Ajg.m3u8 +#EXTINF:-1 tvg-id="",Tele2000 [Not 24/7] +#EXTVLCOPT:http-referrer=https://example2.com/ +https://servilive.com:3126/live/tele2000live.m3u8 diff --git a/tests/__data__/input/playlist_update/cy.m3u b/tests/__data__/input/playlist_update/cy.m3u index 439794ad7..03c605e3e 100644 --- a/tests/__data__/input/playlist_update/cy.m3u +++ b/tests/__data__/input/playlist_update/cy.m3u @@ -1,7 +1,7 @@ -#EXTM3U -#EXTINF:-1 tvg-id="",RIK HD Cyprus -http://l6.cloudskep.com/rikcy/rikhd/playlist.m3u8 -#EXTINF:-1 tvg-id="",RIK 2 -http://l6.cloudskep.com/rikcy/rik2/playlist.m3u8 -#EXTINF:-1 tvg-id="AdaTV.cy",AdaTV -https://ythls.onrender.com/channel/UC40TUSUx490U5uR1lZt3Ajg.m3u8 +#EXTM3U +#EXTINF:-1 tvg-id="",RIK HD Cyprus +http://l6.cloudskep.com/rikcy/rikhd/playlist.m3u8 +#EXTINF:-1 tvg-id="",RIK 2 +http://l6.cloudskep.com/rikcy/rik2/playlist.m3u8 +#EXTINF:-1 tvg-id="AdaTV.cy",AdaTV +https://ythls.onrender.com/channel/UC40TUSUx490U5uR1lZt3Ajg.m3u8 diff --git a/tests/__data__/input/playlist_update/uk.m3u b/tests/__data__/input/playlist_update/uk.m3u index c1deeea97..033f5a36b 100644 --- a/tests/__data__/input/playlist_update/uk.m3u +++ b/tests/__data__/input/playlist_update/uk.m3u @@ -1,5 +1,5 @@ -#EXTM3U -#EXTINF:-1 tvg-id="BBCNews.uk",BBC News HD (720p) [Not 24/7] -http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8 -#EXTINF:-1 tvg-id="BBCNews.uk",BBC News HD (480p) [Geo-blocked] -http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/playlist.m3u8 +#EXTM3U +#EXTINF:-1 tvg-id="BBCNews.uk",BBC News HD (720p) [Not 24/7] +http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8 +#EXTINF:-1 tvg-id="BBCNews.uk",BBC News HD (480p) [Geo-blocked] +http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/playlist.m3u8 diff --git a/tests/__data__/input/playlist_validate/us_blocked.m3u b/tests/__data__/input/playlist_validate/us_blocked.m3u index 2ffa2273b..3a5e4bd53 100644 --- a/tests/__data__/input/playlist_validate/us_blocked.m3u +++ b/tests/__data__/input/playlist_validate/us_blocked.m3u @@ -1,7 +1,7 @@ -#EXTM3U -#EXTINF:-1 tvg-id="FoxSports2.us@Asia",Fox Sports 2 Asia (Thai) (720p) -https://example.com/playlist.m3u8 -#EXTINF:-1 tvg-id="TVN.pl",TVN -https://example.com/playlist2.m3u8 -#EXTINF:-1 tvg-id="EverydayHeroes.us",Everyday Heroes (720p) -https://a.jsrdn.com/broadcast/7b1451fa52/+0000/c.m3u8 +#EXTM3U +#EXTINF:-1 tvg-id="FoxSports2.us@Asia",Fox Sports 2 Asia (Thai) (720p) +https://example.com/playlist.m3u8 +#EXTINF:-1 tvg-id="TVN.pl",TVN +https://example.com/playlist2.m3u8 +#EXTINF:-1 tvg-id="EverydayHeroes.us",Everyday Heroes (720p) +https://a.jsrdn.com/broadcast/7b1451fa52/+0000/c.m3u8 diff --git a/tests/__data__/input/playlist_validate/wrong_id.m3u b/tests/__data__/input/playlist_validate/wrong_id.m3u index fd9867773..dc7ab8504 100644 --- a/tests/__data__/input/playlist_validate/wrong_id.m3u +++ b/tests/__data__/input/playlist_validate/wrong_id.m3u @@ -1,3 +1,3 @@ -#EXTM3U -#EXTINF:-1 tvg-id="qib22lAq1L.us",ABC (720p) -https://example.com/playlist2.m3u8 +#EXTM3U +#EXTINF:-1 tvg-id="qib22lAq1L.us",ABC (720p) +https://example.com/playlist2.m3u8 diff --git a/tests/__data__/input/readme_update/.readme/template.md b/tests/__data__/input/readme_update/.readme/template.md index 04cab4cd5..4883a98e4 100644 --- a/tests/__data__/input/readme_update/.readme/template.md +++ b/tests/__data__/input/readme_update/.readme/template.md @@ -1,158 +1,158 @@ -# IPTV [![update](https://github.com/iptv-org/iptv/actions/workflows/update.yml/badge.svg)](https://github.com/iptv-org/iptv/actions/workflows/update.yml) - -Collection of publicly available IPTV (Internet Protocol television) channels from all over the world. - -## Table of contents - -- 🚀 [How to use?](#how-to-use) -- 📺 [Playlists](#playlists) -- 🗄 [Database](#database) -- 👨‍💻 [API](#api) -- 📚 [Resources](#resources) -- 💬 [Discussions](#discussions) -- ❓ [FAQ](#faq) -- 🛠 [Contribution](#contribution) -- ⚖ [Legal](#legal) -- © [License](#license) - -## How to use? - -Simply insert one of the links below into [any video player](https://github.com/iptv-org/awesome-iptv#apps) that supports live streaming and press _Open_. - -![VLC Network Panel](https://github.com/iptv-org/iptv/raw/master/.readme/preview.png) - -## Playlists - -There are several versions of playlists that differ in the way they are grouped. - -### Main playlist - -Playlist includes all known channels except adult channels. - -``` -https://iptv-org.github.io/iptv/index.m3u -``` - -And here is the full version: - -``` -https://iptv-org.github.io/iptv/index.nsfw.m3u -``` - -### Grouped by category - -
-Expand -
- -Playlist in which each channel has its _category_ as a group title: - -``` -https://iptv-org.github.io/iptv/index.category.m3u -``` - -Same thing, but split up into separate files: - - -#include "tests/__data__/output/.readme/_categories.md" - -
- -### Grouped by language - -
-Expand -
- -Playlist in which each channel has its _language_ as a group title: - -``` -https://iptv-org.github.io/iptv/index.language.m3u -``` - -Same thing, but split up into separate files: - - -#include "tests/__data__/output/.readme/_languages.md" - -
- -### Grouped by country - -
-Expand -
- -Playlist in which each channel has its _country_ as a group title: - -``` -https://iptv-org.github.io/iptv/index.country.m3u -``` - -Same thing, but split up into separate files: - - -#include "tests/__data__/output/.readme/_countries.md" - -
- -### Grouped by region - -
-Expand -
- -Playlist in which each channel has its _region_ as a group title: - -``` -https://iptv-org.github.io/iptv/index.region.m3u -``` - -Same thing, but split up into separate files: - - -#include "tests/__data__/output/.readme/_regions.md" - -
- -## Database - -All channel data is taken from the [iptv-org/database](https://github.com/iptv-org/database) repository. If you find any errors please open a new [issue](https://github.com/iptv-org/database/issues) there. - -## API - -The API documentation can be found in the [iptv-org/api](https://github.com/iptv-org/api) repository. - -## Resources - -Links to other useful IPTV-related resources can be found in the [iptv-org/awesome-iptv](https://github.com/iptv-org/awesome-iptv) repository. - -## Discussions - -If you need help finding a channel, have a question or idea, welcome to the [Discussions](https://github.com/orgs/iptv-org/discussions). - -## FAQ - -The answers to the most popular questions can be found in the [FAQ.md](FAQ.md) file. - -## Contribution - -Please make sure to read the [Contributing Guide](CONTRIBUTING.md) before sending an issue or making a pull request. - -And thank you to everyone who has already contributed! - -### Backers - - - -### Contributors - - - -## Legal - -No video files are stored in this repository. The repository simply contains user-submitted links to publicly available video stream URLs, which to the best of our knowledge have been intentionally made publicly by the copyright holders. If any links in these playlists infringe on your rights as a copyright holder, they may be removed by sending a [pull request](https://github.com/iptv-org/iptv/pulls) or opening an [issue](https://github.com/iptv-org/iptv/issues/new?assignees=freearhey&labels=removal+request&template=--removal-request.yml&title=Remove%3A+). However, note that we have **no control** over the destination of the link, and just removing the link from the playlist will not remove its contents from the web. Note that linking does not directly infringe copyright because no copy is made on the site providing the link, and thus this is **not** a valid reason to send a DMCA notice to GitHub. To remove this content from the web, you should contact the web host that's actually hosting the content (**not** GitHub, nor the maintainers of this repository). - -## License - -[![CC0](http://mirrors.creativecommons.org/presskit/buttons/88x31/svg/cc-zero.svg)](LICENSE) +# IPTV [![update](https://github.com/iptv-org/iptv/actions/workflows/update.yml/badge.svg)](https://github.com/iptv-org/iptv/actions/workflows/update.yml) + +Collection of publicly available IPTV (Internet Protocol television) channels from all over the world. + +## Table of contents + +- 🚀 [How to use?](#how-to-use) +- 📺 [Playlists](#playlists) +- 🗄 [Database](#database) +- 👨‍💻 [API](#api) +- 📚 [Resources](#resources) +- 💬 [Discussions](#discussions) +- ❓ [FAQ](#faq) +- 🛠 [Contribution](#contribution) +- ⚖ [Legal](#legal) +- © [License](#license) + +## How to use? + +Simply insert one of the links below into [any video player](https://github.com/iptv-org/awesome-iptv#apps) that supports live streaming and press _Open_. + +![VLC Network Panel](https://github.com/iptv-org/iptv/raw/master/.readme/preview.png) + +## Playlists + +There are several versions of playlists that differ in the way they are grouped. + +### Main playlist + +Playlist includes all known channels except adult channels. + +``` +https://iptv-org.github.io/iptv/index.m3u +``` + +And here is the full version: + +``` +https://iptv-org.github.io/iptv/index.nsfw.m3u +``` + +### Grouped by category + +
+Expand +
+ +Playlist in which each channel has its _category_ as a group title: + +``` +https://iptv-org.github.io/iptv/index.category.m3u +``` + +Same thing, but split up into separate files: + + +#include "tests/__data__/output/.readme/_categories.md" + +
+ +### Grouped by language + +
+Expand +
+ +Playlist in which each channel has its _language_ as a group title: + +``` +https://iptv-org.github.io/iptv/index.language.m3u +``` + +Same thing, but split up into separate files: + + +#include "tests/__data__/output/.readme/_languages.md" + +
+ +### Grouped by country + +
+Expand +
+ +Playlist in which each channel has its _country_ as a group title: + +``` +https://iptv-org.github.io/iptv/index.country.m3u +``` + +Same thing, but split up into separate files: + + +#include "tests/__data__/output/.readme/_countries.md" + +
+ +### Grouped by region + +
+Expand +
+ +Playlist in which each channel has its _region_ as a group title: + +``` +https://iptv-org.github.io/iptv/index.region.m3u +``` + +Same thing, but split up into separate files: + + +#include "tests/__data__/output/.readme/_regions.md" + +
+ +## Database + +All channel data is taken from the [iptv-org/database](https://github.com/iptv-org/database) repository. If you find any errors please open a new [issue](https://github.com/iptv-org/database/issues) there. + +## API + +The API documentation can be found in the [iptv-org/api](https://github.com/iptv-org/api) repository. + +## Resources + +Links to other useful IPTV-related resources can be found in the [iptv-org/awesome-iptv](https://github.com/iptv-org/awesome-iptv) repository. + +## Discussions + +If you need help finding a channel, have a question or idea, welcome to the [Discussions](https://github.com/orgs/iptv-org/discussions). + +## FAQ + +The answers to the most popular questions can be found in the [FAQ.md](FAQ.md) file. + +## Contribution + +Please make sure to read the [Contributing Guide](CONTRIBUTING.md) before sending an issue or making a pull request. + +And thank you to everyone who has already contributed! + +### Backers + + + +### Contributors + + + +## Legal + +No video files are stored in this repository. The repository simply contains user-submitted links to publicly available video stream URLs, which to the best of our knowledge have been intentionally made publicly by the copyright holders. If any links in these playlists infringe on your rights as a copyright holder, they may be removed by sending a [pull request](https://github.com/iptv-org/iptv/pulls) or opening an [issue](https://github.com/iptv-org/iptv/issues/new?assignees=freearhey&labels=removal+request&template=--removal-request.yml&title=Remove%3A+). However, note that we have **no control** over the destination of the link, and just removing the link from the playlist will not remove its contents from the web. Note that linking does not directly infringe copyright because no copy is made on the site providing the link, and thus this is **not** a valid reason to send a DMCA notice to GitHub. To remove this content from the web, you should contact the web host that's actually hosting the content (**not** GitHub, nor the maintainers of this repository). + +## License + +[![CC0](http://mirrors.creativecommons.org/presskit/buttons/88x31/svg/cc-zero.svg)](LICENSE) diff --git a/tests/__data__/input/readme_update/generators.log b/tests/__data__/input/readme_update/generators.log index 63cfea058..55adb62b0 100644 --- a/tests/__data__/input/readme_update/generators.log +++ b/tests/__data__/input/readme_update/generators.log @@ -1,75 +1,75 @@ -{"type":"category","filepath":"categories/auto.m3u","count":0} -{"type":"category","filepath":"categories/animation.m3u","count":0} -{"type":"category","filepath":"categories/business.m3u","count":0} -{"type":"category","filepath":"categories/classic.m3u","count":0} -{"type":"category","filepath":"categories/comedy.m3u","count":0} -{"type":"category","filepath":"categories/cooking.m3u","count":0} -{"type":"category","filepath":"categories/culture.m3u","count":0} -{"type":"category","filepath":"categories/documentary.m3u","count":0} -{"type":"category","filepath":"categories/education.m3u","count":0} -{"type":"category","filepath":"categories/entertainment.m3u","count":0} -{"type":"category","filepath":"categories/family.m3u","count":0} -{"type":"category","filepath":"categories/general.m3u","count":2} -{"type":"category","filepath":"categories/kids.m3u","count":0} -{"type":"category","filepath":"categories/legislative.m3u","count":0} -{"type":"category","filepath":"categories/lifestyle.m3u","count":0} -{"type":"category","filepath":"categories/movies.m3u","count":0} -{"type":"category","filepath":"categories/music.m3u","count":0} -{"type":"category","filepath":"categories/news.m3u","count":1} -{"type":"category","filepath":"categories/outdoor.m3u","count":0} -{"type":"category","filepath":"categories/relax.m3u","count":0} -{"type":"category","filepath":"categories/religious.m3u","count":0} -{"type":"category","filepath":"categories/series.m3u","count":0} -{"type":"category","filepath":"categories/science.m3u","count":0} -{"type":"category","filepath":"categories/shop.m3u","count":0} -{"type":"category","filepath":"categories/sports.m3u","count":0} -{"type":"category","filepath":"categories/travel.m3u","count":0} -{"type":"category","filepath":"categories/weather.m3u","count":1} -{"type":"category","filepath":"categories/xxx.m3u","count":1} -{"type":"category","filepath":"categories/undefined.m3u","count":3} -{"type":"country","filepath":"countries/cm.m3u","count":1} -{"type":"country","filepath":"countries/ca.m3u","count":2} -{"type":"country","filepath":"countries/cv.m3u","count":1} -{"type":"country","filepath":"countries/cg.m3u","count":1} -{"type":"country","filepath":"countries/ro.m3u","count":1} -{"type":"subdivision","filepath":"subdivisions/ca-on.m3u","count":1} -{"type":"country","filepath":"countries/ru.m3u","count":2} -{"type":"country","filepath":"countries/rw.m3u","count":1} -{"type":"country","filepath":"countries/re.m3u","count":1} -{"type":"country","filepath":"countries/undefined.m3u","count":2} -{"type":"country","filepath":"countries/bl.m3u","count":1} -{"type":"country","filepath":"countries/sh.m3u","count":1} -{"type":"country","filepath":"countries/kn.m3u","count":1} -{"type":"language","filepath":"languages/cat.m3u","count":1} -{"type":"language","filepath":"languages/eng.m3u","count":1} -{"type":"language","filepath":"languages/fra.m3u","count":1} -{"type":"language","filepath":"languages/rus.m3u","count":1} -{"type":"language","filepath":"languages/undefined.m3u","count":2} -{"type":"region","filepath":"regions/afr.m3u","count":0} -{"type":"region","filepath":"regions/amer.m3u","count":1} -{"type":"region","filepath":"regions/apac.m3u","count":1} -{"type":"region","filepath":"regions/arab.m3u","count":0} -{"type":"region","filepath":"regions/asean.m3u","count":0} -{"type":"region","filepath":"regions/asia.m3u","count":2} -{"type":"region","filepath":"regions/carib.m3u","count":0} -{"type":"region","filepath":"regions/cas.m3u","count":0} -{"type":"region","filepath":"regions/cenamer.m3u","count":0} -{"type":"region","filepath":"regions/cis.m3u","count":1} -{"type":"region","filepath":"regions/emea.m3u","count":3} -{"type":"region","filepath":"regions/eur.m3u","count":3} -{"type":"region","filepath":"regions/hispam.m3u","count":0} -{"type":"region","filepath":"regions/lac.m3u","count":0} -{"type":"region","filepath":"regions/latam.m3u","count":0} -{"type":"region","filepath":"regions/maghreb.m3u","count":0} -{"type":"region","filepath":"regions/mena.m3u","count":0} -{"type":"region","filepath":"regions/mideast.m3u","count":0} -{"type":"region","filepath":"regions/nam.m3u","count":1} -{"type":"region","filepath":"regions/noram.m3u","count":1} -{"type":"region","filepath":"regions/nord.m3u","count":0} -{"type":"region","filepath":"regions/oce.m3u","count":0} -{"type":"region","filepath":"regions/undefined.m3u","count":2} -{"type":"region","filepath":"regions/sas.m3u","count":1} -{"type":"region","filepath":"regions/int.m3u","count":1} -{"type":"region","filepath":"regions/southam.m3u","count":0} -{"type":"region","filepath":"regions/ssa.m3u","count":0} +{"type":"category","filepath":"categories/auto.m3u","count":0} +{"type":"category","filepath":"categories/animation.m3u","count":0} +{"type":"category","filepath":"categories/business.m3u","count":0} +{"type":"category","filepath":"categories/classic.m3u","count":0} +{"type":"category","filepath":"categories/comedy.m3u","count":0} +{"type":"category","filepath":"categories/cooking.m3u","count":0} +{"type":"category","filepath":"categories/culture.m3u","count":0} +{"type":"category","filepath":"categories/documentary.m3u","count":0} +{"type":"category","filepath":"categories/education.m3u","count":0} +{"type":"category","filepath":"categories/entertainment.m3u","count":0} +{"type":"category","filepath":"categories/family.m3u","count":0} +{"type":"category","filepath":"categories/general.m3u","count":2} +{"type":"category","filepath":"categories/kids.m3u","count":0} +{"type":"category","filepath":"categories/legislative.m3u","count":0} +{"type":"category","filepath":"categories/lifestyle.m3u","count":0} +{"type":"category","filepath":"categories/movies.m3u","count":0} +{"type":"category","filepath":"categories/music.m3u","count":0} +{"type":"category","filepath":"categories/news.m3u","count":1} +{"type":"category","filepath":"categories/outdoor.m3u","count":0} +{"type":"category","filepath":"categories/relax.m3u","count":0} +{"type":"category","filepath":"categories/religious.m3u","count":0} +{"type":"category","filepath":"categories/series.m3u","count":0} +{"type":"category","filepath":"categories/science.m3u","count":0} +{"type":"category","filepath":"categories/shop.m3u","count":0} +{"type":"category","filepath":"categories/sports.m3u","count":0} +{"type":"category","filepath":"categories/travel.m3u","count":0} +{"type":"category","filepath":"categories/weather.m3u","count":1} +{"type":"category","filepath":"categories/xxx.m3u","count":1} +{"type":"category","filepath":"categories/undefined.m3u","count":3} +{"type":"country","filepath":"countries/cm.m3u","count":1} +{"type":"country","filepath":"countries/ca.m3u","count":2} +{"type":"country","filepath":"countries/cv.m3u","count":1} +{"type":"country","filepath":"countries/cg.m3u","count":1} +{"type":"country","filepath":"countries/ro.m3u","count":1} +{"type":"subdivision","filepath":"subdivisions/ca-on.m3u","count":1} +{"type":"country","filepath":"countries/ru.m3u","count":2} +{"type":"country","filepath":"countries/rw.m3u","count":1} +{"type":"country","filepath":"countries/re.m3u","count":1} +{"type":"country","filepath":"countries/undefined.m3u","count":2} +{"type":"country","filepath":"countries/bl.m3u","count":1} +{"type":"country","filepath":"countries/sh.m3u","count":1} +{"type":"country","filepath":"countries/kn.m3u","count":1} +{"type":"language","filepath":"languages/cat.m3u","count":1} +{"type":"language","filepath":"languages/eng.m3u","count":1} +{"type":"language","filepath":"languages/fra.m3u","count":1} +{"type":"language","filepath":"languages/rus.m3u","count":1} +{"type":"language","filepath":"languages/undefined.m3u","count":2} +{"type":"region","filepath":"regions/afr.m3u","count":0} +{"type":"region","filepath":"regions/amer.m3u","count":1} +{"type":"region","filepath":"regions/apac.m3u","count":1} +{"type":"region","filepath":"regions/arab.m3u","count":0} +{"type":"region","filepath":"regions/asean.m3u","count":0} +{"type":"region","filepath":"regions/asia.m3u","count":2} +{"type":"region","filepath":"regions/carib.m3u","count":0} +{"type":"region","filepath":"regions/cas.m3u","count":0} +{"type":"region","filepath":"regions/cenamer.m3u","count":0} +{"type":"region","filepath":"regions/cis.m3u","count":1} +{"type":"region","filepath":"regions/emea.m3u","count":3} +{"type":"region","filepath":"regions/eur.m3u","count":3} +{"type":"region","filepath":"regions/hispam.m3u","count":0} +{"type":"region","filepath":"regions/lac.m3u","count":0} +{"type":"region","filepath":"regions/latam.m3u","count":0} +{"type":"region","filepath":"regions/maghreb.m3u","count":0} +{"type":"region","filepath":"regions/mena.m3u","count":0} +{"type":"region","filepath":"regions/mideast.m3u","count":0} +{"type":"region","filepath":"regions/nam.m3u","count":1} +{"type":"region","filepath":"regions/noram.m3u","count":1} +{"type":"region","filepath":"regions/nord.m3u","count":0} +{"type":"region","filepath":"regions/oce.m3u","count":0} +{"type":"region","filepath":"regions/undefined.m3u","count":2} +{"type":"region","filepath":"regions/sas.m3u","count":1} +{"type":"region","filepath":"regions/int.m3u","count":1} +{"type":"region","filepath":"regions/southam.m3u","count":0} +{"type":"region","filepath":"regions/ssa.m3u","count":0} {"type":"region","filepath":"regions/wafr.m3u","count":0} \ No newline at end of file diff --git a/tests/__data__/input/report_create/br.m3u b/tests/__data__/input/report_create/br.m3u index 6dcdaee15..31a869861 100644 --- a/tests/__data__/input/report_create/br.m3u +++ b/tests/__data__/input/report_create/br.m3u @@ -1,6 +1,6 @@ -#EXTM3U -#EXTINF:-1 tvg-id="",VTV [Not 24/7] -https://ythls.onrender.com/channel/UC40TUSUx490U5uR1lZt3Ajg.m3u8 -#EXTINF:-1 tvg-id="",Tele2000 [Not 24/7] -#EXTVLCOPT:http-referrer=https://example2.com/ -https://servilive.com:3126/live/tele2000live.m3u8 +#EXTM3U +#EXTINF:-1 tvg-id="",VTV [Not 24/7] +https://ythls.onrender.com/channel/UC40TUSUx490U5uR1lZt3Ajg.m3u8 +#EXTINF:-1 tvg-id="",Tele2000 [Not 24/7] +#EXTVLCOPT:http-referrer=https://example2.com/ +https://servilive.com:3126/live/tele2000live.m3u8 diff --git a/tests/__data__/input/report_create/cy.m3u b/tests/__data__/input/report_create/cy.m3u index 581d0961b..82d0b78d4 100644 --- a/tests/__data__/input/report_create/cy.m3u +++ b/tests/__data__/input/report_create/cy.m3u @@ -1,5 +1,5 @@ -#EXTM3U -#EXTINF:-1 tvg-id="",RIK HD Cyprus -http://l6.cloudskep.com/rikcy/rikhd/playlist.m3u8 -#EXTINF:-1 tvg-id="",RIK 2 -http://l6.cloudskep.com/rikcy/rik2/playlist.m3u8 +#EXTM3U +#EXTINF:-1 tvg-id="",RIK HD Cyprus +http://l6.cloudskep.com/rikcy/rikhd/playlist.m3u8 +#EXTINF:-1 tvg-id="",RIK 2 +http://l6.cloudskep.com/rikcy/rik2/playlist.m3u8 diff --git a/tests/__data__/input/report_create/uk.m3u b/tests/__data__/input/report_create/uk.m3u index c1deeea97..033f5a36b 100644 --- a/tests/__data__/input/report_create/uk.m3u +++ b/tests/__data__/input/report_create/uk.m3u @@ -1,5 +1,5 @@ -#EXTM3U -#EXTINF:-1 tvg-id="BBCNews.uk",BBC News HD (720p) [Not 24/7] -http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8 -#EXTINF:-1 tvg-id="BBCNews.uk",BBC News HD (480p) [Geo-blocked] -http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/playlist.m3u8 +#EXTM3U +#EXTINF:-1 tvg-id="BBCNews.uk",BBC News HD (720p) [Not 24/7] +http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/index.m3u8 +#EXTINF:-1 tvg-id="BBCNews.uk",BBC News HD (480p) [Geo-blocked] +http://1111296894.rsc.cdn77.org/LS-ATL-54548-6/playlist.m3u8 diff --git a/tests/__data__/input/report_create/us.m3u b/tests/__data__/input/report_create/us.m3u index 92cee65b0..78ea4deb5 100644 --- a/tests/__data__/input/report_create/us.m3u +++ b/tests/__data__/input/report_create/us.m3u @@ -1,3 +1,3 @@ -#EXTM3U -#EXTINF:-1 tvg-id="",TUTV -https://livestream.telvue.com/templeuni1/f7b44cfafd5c52223d5498196c8a2e7b.sdp/playlist.m3u8 +#EXTM3U +#EXTINF:-1 tvg-id="",TUTV +https://livestream.telvue.com/templeuni1/f7b44cfafd5c52223d5498196c8a2e7b.sdp/playlist.m3u8 diff --git a/tests/commands/api/generate.test.ts b/tests/commands/api/generate.test.ts index 7f8f68aa9..931f543cb 100644 --- a/tests/commands/api/generate.test.ts +++ b/tests/commands/api/generate.test.ts @@ -1,25 +1,31 @@ +import { pathToFileURL } from 'node:url' import { execSync } from 'child_process' import fs from 'fs-extra' +import os from 'os' + +let ENV_VAR = + 'DATA_DIR=tests/__data__/input/data STREAMS_DIR=tests/__data__/input/api_generate API_DIR=tests/__data__/output/.api' +if (os.platform() === 'win32') { + ENV_VAR = + 'SET "DATA_DIR=tests/__data__/input/data" && SET "STREAMS_DIR=tests/__data__/input/api_generate" && SET "API_DIR=tests/__data__/output/.api" &&' +} beforeEach(() => { fs.emptyDirSync('tests/__data__/output') }) -it('can create streams.json', () => { - execSync( - 'DATA_DIR=tests/__data__/input/data STREAMS_DIR=tests/__data__/input/api_generate API_DIR=tests/__data__/output/.api npm run api:generate', - { encoding: 'utf8' } - ) +describe('api:generate', () => { + it('can create streams.json', () => { + const cmd = `${ENV_VAR} npm run api:generate` + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) - expect(content('output/.api/streams.json')).toMatchObject( - content('expected/api_generate/.api/streams.json') - ) + expect(content('tests/__data__/output/.api/streams.json')).toMatchObject( + content('tests/__data__/expected/api_generate/.api/streams.json') + ) + }) }) function content(filepath: string) { - return JSON.parse( - fs.readFileSync(`tests/__data__/${filepath}`, { - encoding: 'utf8' - }) - ) + return JSON.parse(fs.readFileSync(pathToFileURL(filepath), { encoding: 'utf8' })) } diff --git a/tests/commands/playlist/edit.test.ts b/tests/commands/playlist/edit.test.ts new file mode 100644 index 000000000..3334b6dee --- /dev/null +++ b/tests/commands/playlist/edit.test.ts @@ -0,0 +1,42 @@ +import { execSync } from 'child_process' +import fs from 'fs-extra' +import os from 'os' + +type ExecError = { + status: number + stdout: string +} + +let ENV_VAR = 'DATA_DIR=tests/__data__/input/data' +if (os.platform() === 'win32') { + ENV_VAR = 'SET "DATA_DIR=tests/__data__/input/data" &&' +} + +beforeEach(() => { + fs.emptyDirSync('tests/__data__/output') + fs.copySync( + 'tests/__data__/input/playlist_edit/playlist.m3u', + 'tests/__data__/output/playlist.m3u' + ) +}) + +describe('playlist:edit', () => { + it('shows list of options for a streams', () => { + const cmd = `${ENV_VAR} npm run playlist:edit --- tests/__data__/output/playlist.m3u` + try { + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) + checkStdout(stdout) + } catch (error) { + // NOTE: for Windows only + if (process.env.DEBUG === 'true') console.log(cmd, error) + checkStdout((error as ExecError).stdout) + } + }) +}) + +function checkStdout(stdout: string) { + expect(stdout).toContain('TF1.fr (TF1, Télévision française 1)') + expect(stdout).toContain('Type...') + expect(stdout).toContain('Skip') +} diff --git a/tests/commands/playlist/format.test.ts b/tests/commands/playlist/format.test.ts index d513c8b38..f6c631252 100644 --- a/tests/commands/playlist/format.test.ts +++ b/tests/commands/playlist/format.test.ts @@ -1,30 +1,40 @@ +import { pathToFileURL } from 'node:url' import { execSync } from 'child_process' import * as fs from 'fs-extra' import { glob } from 'glob' +import os from 'os' + +let ENV_VAR = 'STREAMS_DIR=tests/__data__/output/streams' +if (os.platform() === 'win32') { + ENV_VAR = 'SET "STREAMS_DIR=tests/__data__/output/streams" &&' +} beforeEach(() => { fs.emptyDirSync('tests/__data__/output') fs.copySync('tests/__data__/input/playlist_format', 'tests/__data__/output/streams') }) -it('can format playlists', () => { - execSync('STREAMS_DIR=tests/__data__/output/streams npm run playlist:format', { - encoding: 'utf8' - }) +describe('playlist:format', () => { + it('can format playlists', () => { + const cmd = `${ENV_VAR} npm run playlist:format` + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) - const files = glob - .sync('tests/__data__/expected/playlist_format/*.m3u') - .map(f => f.replace('tests/__data__/expected/playlist_format/', '')) + const files = glob.sync('tests/__data__/expected/playlist_format/*.m3u').map(filepath => { + const fileUrl = pathToFileURL(filepath).toString() + const pathToRemove = pathToFileURL('tests/__data__/expected/playlist_format/').toString() - files.forEach(filepath => { - expect(content(`output/streams/${filepath}`), filepath).toBe( - content(`expected/playlist_format/${filepath}`) - ) + return fileUrl.replace(pathToRemove, '') + }) + + files.forEach(filepath => { + expect(content(`tests/__data__/output/streams/${filepath}`), filepath).toBe( + content(`tests/__data__/expected/playlist_format/${filepath}`) + ) + }) }) }) function content(filepath: string) { - return fs.readFileSync(`tests/__data__/${filepath}`, { - encoding: 'utf8' - }) + return fs.readFileSync(pathToFileURL(filepath), { encoding: 'utf8' }) } diff --git a/tests/commands/playlist/generate.test.ts b/tests/commands/playlist/generate.test.ts index 54c4e61e0..4e14d0d30 100644 --- a/tests/commands/playlist/generate.test.ts +++ b/tests/commands/playlist/generate.test.ts @@ -1,34 +1,47 @@ +import { pathToFileURL } from 'node:url' import { execSync } from 'child_process' +import os, { EOL } from 'node:os' import * as fs from 'fs-extra' import * as glob from 'glob' +let ENV_VAR = + 'STREAMS_DIR=tests/__data__/input/playlist_generate DATA_DIR=tests/__data__/input/data PUBLIC_DIR=tests/__data__/output/.gh-pages LOGS_DIR=tests/__data__/output/logs' +if (os.platform() === 'win32') { + ENV_VAR = + 'SET "STREAMS_DIR=tests/__data__/input/playlist_generate" && SET "DATA_DIR=tests/__data__/input/data" && SET "PUBLIC_DIR=tests/__data__/output/.gh-pages" && SET "LOGS_DIR=tests/__data__/output/logs" &&' +} + beforeEach(() => { fs.emptyDirSync('tests/__data__/output') }) -it('can generate playlists and logs', () => { - execSync( - 'STREAMS_DIR=tests/__data__/input/playlist_generate DATA_DIR=tests/__data__/input/data PUBLIC_DIR=tests/__data__/output/.gh-pages LOGS_DIR=tests/__data__/output/logs npm run playlist:generate', - { encoding: 'utf8' } - ) +describe('playlist:generate', () => { + it('can generate playlists and logs', () => { + const cmd = `${ENV_VAR} npm run playlist:generate` + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) - const playlists = glob - .sync('tests/__data__/expected/playlist_generate/.gh-pages/**/*.m3u') - .map((file: string) => file.replace('tests/__data__/expected/playlist_generate/', '')) + const playlists = glob + .sync('tests/__data__/expected/playlist_generate/.gh-pages/**/*.m3u') + .map(filepath => { + const fileUrl = pathToFileURL(filepath).toString() + const pathToRemove = pathToFileURL('tests/__data__/expected/playlist_generate/').toString() - playlists.forEach((filepath: string) => { - expect(content(`output/${filepath}`), filepath).toBe( - content(`expected/playlist_generate/${filepath}`) + return fileUrl.replace(pathToRemove, '') + }) + + playlists.forEach((filepath: string) => { + expect(content(`tests/__data__/output/${filepath}`), filepath).toBe( + content(`tests/__data__/expected/playlist_generate/${filepath}`) + ) + }) + + expect(content('tests/__data__/output/logs/generators.log').split(EOL).sort()).toStrictEqual( + content('tests/__data__/expected/playlist_generate/logs/generators.log').split(EOL).sort() ) }) - - expect(content('output/logs/generators.log').split('\n').sort()).toStrictEqual( - content('expected/playlist_generate/logs/generators.log').split('\n').sort() - ) }) function content(filepath: string) { - return fs.readFileSync(`tests/__data__/${filepath}`, { - encoding: 'utf8' - }) + return fs.readFileSync(pathToFileURL(filepath), { encoding: 'utf8' }) } diff --git a/tests/commands/playlist/test.test.ts b/tests/commands/playlist/test.test.ts index 0842f0f10..813ed401d 100644 --- a/tests/commands/playlist/test.test.ts +++ b/tests/commands/playlist/test.test.ts @@ -1,19 +1,25 @@ import { execSync } from 'child_process' +import os from 'node:os' type ExecError = { status: number stdout: string } -it('shows an error if the playlist contains a broken link', () => { - try { - execSync('ROOT_DIR=tests/__data__/input npm run playlist:test playlist_test/ag.m3u', { - encoding: 'utf8' - }) - process.exit(1) - } catch (error) { - expect((error as ExecError).status).toBe(1) - expect((error as ExecError).stdout).toContain('playlist_test/ag.m3u') - expect((error as ExecError).stdout).toContain('2 problems (1 errors, 1 warnings)') - } +let ENV_VAR = 'ROOT_DIR=tests/__data__/input' +if (os.platform() === 'win32') { + ENV_VAR = 'SET "ROOT_DIR=tests/__data__/input" &&' +} + +describe('playlist:test', () => { + it('shows an error if the playlist contains a broken link', () => { + const cmd = `${ENV_VAR} npm run playlist:test playlist_test/ag.m3u` + try { + execSync(cmd, { encoding: 'utf8' }) + } catch (error) { + if (process.env.DEBUG === 'true') console.log(cmd, error) + expect((error as ExecError).stdout).toContain('playlist_test/ag.m3u') + expect((error as ExecError).stdout).toContain('2 problems (1 errors, 1 warnings)') + } + }) }) diff --git a/tests/commands/playlist/update.test.ts b/tests/commands/playlist/update.test.ts index cc104d8cc..a430b8df4 100644 --- a/tests/commands/playlist/update.test.ts +++ b/tests/commands/playlist/update.test.ts @@ -1,37 +1,45 @@ +import { pathToFileURL } from 'node:url' import { execSync } from 'child_process' import * as fs from 'fs-extra' import { glob } from 'glob' +import os from 'os' + +let ENV_VAR = 'DATA_DIR=tests/__data__/input/data STREAMS_DIR=tests/__data__/output/streams' +if (os.platform() === 'win32') { + ENV_VAR = + 'SET "DATA_DIR=tests/__data__/input/data" && SET "STREAMS_DIR=tests/__data__/output/streams" &&' +} beforeEach(() => { fs.emptyDirSync('tests/__data__/output') fs.copySync('tests/__data__/input/playlist_update', 'tests/__data__/output/streams') }) -it('can update playlists', () => { - const stdout = execSync( - 'DATA_DIR=tests/__data__/input/data STREAMS_DIR=tests/__data__/output/streams npm run playlist:update --silent', - { - encoding: 'utf8' - } - ) +describe('playlist:update', () => { + it('can update playlists', () => { + const cmd = `${ENV_VAR} npm run playlist:update --silent` + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) - const files = glob - .sync('tests/__data__/expected/playlist_update/*.m3u') - .map(f => f.replace('tests/__data__/expected/playlist_update/', '')) + const files = glob.sync('tests/__data__/expected/playlist_update/*.m3u').map(filepath => { + const fileUrl = pathToFileURL(filepath).toString() + const pathToRemove = pathToFileURL('tests/__data__/expected/playlist_update/').toString() - files.forEach(filepath => { - expect(content(`output/streams/${filepath}`), filepath).toBe( - content(`expected/playlist_update/${filepath}`) + return fileUrl.replace(pathToRemove, '') + }) + + files.forEach(filepath => { + expect(content(`tests/__data__/output/streams/${filepath}`), filepath).toBe( + content(`tests/__data__/expected/playlist_update/${filepath}`) + ) + }) + + expect(stdout).toBe( + 'OUTPUT=closes #14151, closes #14150, closes #14110, closes #14120, closes #14175, closes #14105, closes #14104, closes #14057, closes #14034, closes #13964, closes #13893, closes #13881, closes #13793, closes #13751, closes #13715\n' ) }) - - expect(stdout).toBe( - 'OUTPUT=closes #14151, closes #14150, closes #14110, closes #14120, closes #14175, closes #14105, closes #14104, closes #14057, closes #14034, closes #13964, closes #13893, closes #13881, closes #13793, closes #13751, closes #13715\n' - ) }) function content(filepath: string) { - return fs.readFileSync(`tests/__data__/${filepath}`, { - encoding: 'utf8' - }) + return fs.readFileSync(pathToFileURL(filepath), { encoding: 'utf8' }) } diff --git a/tests/commands/playlist/validate.test.ts b/tests/commands/playlist/validate.test.ts index 38dd104ee..feef97bbf 100644 --- a/tests/commands/playlist/validate.test.ts +++ b/tests/commands/playlist/validate.test.ts @@ -1,38 +1,45 @@ import { execSync } from 'child_process' +import os from 'os' type ExecError = { status: number stdout: string } -it('show an error if channel id in the blocklist', () => { - try { - execSync( - 'DATA_DIR=tests/__data__/input/data STREAMS_DIR=tests/__data__/input/playlist_validate npm run playlist:validate -- us_blocked.m3u', - { - encoding: 'utf8' - } - ) - process.exit(1) - } catch (error) { - expect((error as ExecError).status).toBe(1) - expect((error as ExecError).stdout).toContain(`us_blocked.m3u - 2 error "FoxSports2.us" is on the blocklist due to claims of copyright holders (https://github.com/iptv-org/iptv/issues/0002) - 4 error "TVN.pl" is on the blocklist due to NSFW content (https://github.com/iptv-org/iptv/issues/0003) +let ENV_VAR = + 'DATA_DIR=tests/__data__/input/data STREAMS_DIR=tests/__data__/input/playlist_validate' +if (os.platform() === 'win32') { + ENV_VAR = + 'SET "DATA_DIR=tests/__data__/input/data" && SET "STREAMS_DIR=tests/__data__/input/playlist_validate" &&' +} -2 problems (2 errors, 0 warnings)`) - } -}) - -it('show a warning if channel has wrong id', () => { - const stdout = execSync( - 'DATA_DIR=tests/__data__/input/data STREAMS_DIR=tests/__data__/input/playlist_validate npm run playlist:validate -- wrong_id.m3u', - { - encoding: 'utf8' +describe('playlist:validate', () => { + it('show an error if channel id in the blocklist', () => { + const cmd = `${ENV_VAR} npm run playlist:validate -- us_blocked.m3u` + try { + execSync(cmd, { encoding: 'utf8' }) + } catch (error) { + if (process.env.DEBUG === 'true') console.log(cmd, error) + expect((error as ExecError).stdout).toContain('us_blocked.m3u') + expect((error as ExecError).stdout).toContain( + '2 error "FoxSports2.us" is on the blocklist due to claims of copyright holders (https://github.com/iptv-org/iptv/issues/0002)' + ) + expect((error as ExecError).stdout).toContain( + '4 error "TVN.pl" is on the blocklist due to NSFW content (https://github.com/iptv-org/iptv/issues/0003)' + ) + expect((error as ExecError).stdout).toContain('2 problems (2 errors, 0 warnings)') } - ) + }) - expect(stdout).toContain( - 'wrong_id.m3u\n 2 warning "qib22lAq1L.us" is not in the database\n\n1 problems (0 errors, 1 warnings)\n' - ) + it('show a warning if channel has wrong id', () => { + const cmd = `${ENV_VAR} npm run playlist:validate -- wrong_id.m3u` + try { + execSync(cmd, { encoding: 'utf8' }) + } catch (error) { + if (process.env.DEBUG === 'true') console.log(cmd, error) + expect((error as ExecError).stdout).toContain( + 'wrong_id.m3u\n 2 warning "qib22lAq1L.us" is not in the database\n\n1 problems (0 errors, 1 warnings)\n' + ) + } + }) }) diff --git a/tests/commands/readme/update.test.ts b/tests/commands/readme/update.test.ts index f15e71f48..061179ed4 100644 --- a/tests/commands/readme/update.test.ts +++ b/tests/commands/readme/update.test.ts @@ -1,6 +1,15 @@ +import { pathToFileURL } from 'node:url' import { execSync } from 'child_process' import fs from 'fs-extra' import path from 'path' +import os from 'os' + +let ENV_VAR = + 'DATA_DIR=tests/__data__/input/data LOGS_DIR=tests/__data__/input/readme_update README_DIR=tests/__data__/output/.readme' +if (os.platform() === 'win32') { + ENV_VAR = + 'SET "DATA_DIR=tests/__data__/input/data" && SET "LOGS_DIR=tests/__data__/input/readme_update" && SET "README_DIR=tests/__data__/output/.readme" &&' +} beforeEach(() => { fs.emptyDirSync('tests/__data__/output') @@ -13,23 +22,20 @@ beforeEach(() => { 'tests/__data__/input/readme_update/.readme/template.md', 'tests/__data__/output/.readme/template.md' ) - - execSync( - 'DATA_DIR=tests/__data__/input/data LOGS_DIR=tests/__data__/input/readme_update README_DIR=tests/__data__/output/.readme npm run readme:update', - { encoding: 'utf8' } - ) }) -it('can update readme.md', () => { - expect(content('tests/__data__/output/readme.md')).toEqual( - content('tests/__data__/expected/readme_update/_readme.md') - ) +describe('readme:update', () => { + it('can update readme.md', () => { + const cmd = `${ENV_VAR} npm run readme:update` + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) + + expect(content('tests/__data__/output/readme.md')).toEqual( + content('tests/__data__/expected/readme_update/_readme.md') + ) + }) }) function content(filepath: string) { - const data = fs.readFileSync(path.resolve(filepath), { - encoding: 'utf8' - }) - - return JSON.stringify(data) + return JSON.stringify(fs.readFileSync(pathToFileURL(filepath), { encoding: 'utf8' })) } diff --git a/tests/commands/report/create.test.ts b/tests/commands/report/create.test.ts index de449347a..18c799445 100644 --- a/tests/commands/report/create.test.ts +++ b/tests/commands/report/create.test.ts @@ -1,15 +1,20 @@ import { execSync } from 'child_process' +import os from 'os' -it('can create report', () => { - const stdout = execSync( - 'DATA_DIR=tests/__data__/input/data STREAMS_DIR=tests/__data__/input/report_create npm run report:create', - { - encoding: 'utf8' - } - ) +let ENV_VAR = 'DATA_DIR=tests/__data__/input/data STREAMS_DIR=tests/__data__/input/report_create' +if (os.platform() === 'win32') { + ENV_VAR = + 'SET "DATA_DIR=tests/__data__/input/data" && SET "STREAMS_DIR=tests/__data__/input/report_create" &&' +} - expect( - stdout.includes(` +describe('report:create', () => { + it('can create report', () => { + const cmd = `${ENV_VAR} npm run report:create` + const stdout = execSync(cmd, { encoding: 'utf8' }) + if (process.env.DEBUG === 'true') console.log(cmd, stdout) + + expect( + stdout.includes(` ┌─────────┬─────────────┬──────────────────┬─────────────────────────────┬───────────────────────────────────────────────────────────────────────────────────────────────────────────┬───────────────┐ │ (index) │ issueNumber │ type │ streamId │ streamUrl │ status │ ├─────────┼─────────────┼──────────────────┼─────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────┼───────────────┤ @@ -20,5 +25,6 @@ it('can create report', () => { │ 4 │ 16120 │ 'broken stream' │ undefined │ 'http://190.61.102.67:2000/play/a038/index.m3u8' │ 'wrong_link' │ │ 5 │ 19956 │ 'channel search' │ 'CNBCe.tr' │ undefined │ 'invalid_id' │ └─────────┴─────────────┴──────────────────┴─────────────────────────────┴───────────────────────────────────────────────────────────────────────────────────────────────────────────┴───────────────┘`) - ).toBe(true) + ).toBe(true) + }) }) diff --git a/yarn.lock b/yarn.lock index 82088c20d..188beea60 100644 --- a/yarn.lock +++ b/yarn.lock @@ -366,24 +366,28 @@ "@eslint/core" "^0.12.0" levn "^0.4.1" -"@freearhey/core@^0.7.0": - version "0.7.0" - resolved "https://registry.npmjs.org/@freearhey/core/-/core-0.7.0.tgz" - integrity sha512-HXkKPYGY7ife7JAc1q/Qxzy0WUdSnyt3rHThCShZHgnH3rz0tpkjHFW7LNegB3he0IKn/Zc95/YSOQ97Fq8ctA== +"@freearhey/core@^0.8.2": + version "0.8.2" + resolved "https://registry.npmjs.org/@freearhey/core/-/core-0.8.2.tgz" + integrity sha512-jlb1XUbhUf3lqD3B9Wmx3c8qYG4+s1I0cr2FFQfiMpJh4nMvfUNdJr2OhH31S/dbNP12ycT6RPVoZ2j2G3+mXA== dependencies: - "@types/fs-extra" "^11.0.2" - "@types/lodash" "^4.14.198" - "@types/luxon" "^3.3.2" - fs-extra "^11.1.1" - glob "^10.3.4" + consola "^3.4.2" + dayjs "^1.11.13" + fs-extra "^11.3.0" + glob "^11.0.1" lodash "^4.17.21" - luxon "^3.4.3" - natural-orderby "^3.0.2" - node-gzip "^1.1.2" + natural-orderby "^5.0.0" normalize-url "^6.1.0" object-treeify "^2.1.1" - run-script-os "^1.1.6" - signale "^1.4.0" + pako "^2.1.0" + timer-node "^5.0.9" + +"@freearhey/search-js@^0.1.2": + version "0.1.2" + resolved "https://registry.npmjs.org/@freearhey/search-js/-/search-js-0.1.2.tgz" + integrity sha512-F2o+xpGCXOK4OsZfKEHfXNNkAZmny2eBnPOp+P0iyV20ja7gJGfTFaEc6okcuEo6OB6P7LnSxTvISkoArFtlfg== + dependencies: + lodash "^4.17.21" "@humanfs/core@^0.19.1": version "0.19.1" @@ -413,21 +417,32 @@ resolved "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz" integrity sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ== -"@inquirer/confirm@^5.0.0": - version "5.1.7" - resolved "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.7.tgz" - integrity sha512-Xrfbrw9eSiHb+GsesO8TQIeHSMTP0xyvTCeeYevgZ4sKW+iz9w/47bgfG9b0niQm+xaLY2EWPBINUPldLwvYiw== +"@inquirer/checkbox@^4.1.5": + version "4.1.5" + resolved "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.5.tgz" + integrity sha512-swPczVU+at65xa5uPfNP9u3qx/alNwiaykiI/ExpsmMSQW55trmZcwhYWzw/7fj+n6Q8z1eENvR7vFfq9oPSAQ== dependencies: - "@inquirer/core" "^10.1.8" - "@inquirer/type" "^3.0.5" + "@inquirer/core" "^10.1.10" + "@inquirer/figures" "^1.0.11" + "@inquirer/type" "^3.0.6" + ansi-escapes "^4.3.2" + yoctocolors-cjs "^2.1.2" -"@inquirer/core@^10.1.8": - version "10.1.8" - resolved "https://registry.npmjs.org/@inquirer/core/-/core-10.1.8.tgz" - integrity sha512-HpAqR8y715zPpM9e/9Q+N88bnGwqqL8ePgZ0SMv/s3673JLMv3bIkoivGmjPqXlEgisUksSXibweQccUwEx4qQ== +"@inquirer/confirm@^5.0.0", "@inquirer/confirm@^5.1.9": + version "5.1.9" + resolved "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.9.tgz" + integrity sha512-NgQCnHqFTjF7Ys2fsqK2WtnA8X1kHyInyG+nMIuHowVTIgIuS10T4AznI/PvbqSpJqjCUqNBlKGh1v3bwLFL4w== + dependencies: + "@inquirer/core" "^10.1.10" + "@inquirer/type" "^3.0.6" + +"@inquirer/core@^10.1.10": + version "10.1.10" + resolved "https://registry.npmjs.org/@inquirer/core/-/core-10.1.10.tgz" + integrity sha512-roDaKeY1PYY0aCqhRmXihrHjoSW2A00pV3Ke5fTpMCkzcGF64R8e0lw3dK+eLEHwS4vB5RnW1wuQmvzoRul8Mw== dependencies: "@inquirer/figures" "^1.0.11" - "@inquirer/type" "^3.0.5" + "@inquirer/type" "^3.0.6" ansi-escapes "^4.3.2" cli-width "^4.1.0" mute-stream "^2.0.0" @@ -435,15 +450,104 @@ wrap-ansi "^6.2.0" yoctocolors-cjs "^2.1.2" +"@inquirer/editor@^4.2.10": + version "4.2.10" + resolved "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.10.tgz" + integrity sha512-5GVWJ+qeI6BzR6TIInLP9SXhWCEcvgFQYmcRG6d6RIlhFjM5TyG18paTGBgRYyEouvCmzeco47x9zX9tQEofkw== + dependencies: + "@inquirer/core" "^10.1.10" + "@inquirer/type" "^3.0.6" + external-editor "^3.1.0" + +"@inquirer/expand@^4.0.12": + version "4.0.12" + resolved "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.12.tgz" + integrity sha512-jV8QoZE1fC0vPe6TnsOfig+qwu7Iza1pkXoUJ3SroRagrt2hxiL+RbM432YAihNR7m7XnU0HWl/WQ35RIGmXHw== + dependencies: + "@inquirer/core" "^10.1.10" + "@inquirer/type" "^3.0.6" + yoctocolors-cjs "^2.1.2" + "@inquirer/figures@^1.0.11": version "1.0.11" resolved "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.11.tgz" integrity sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw== -"@inquirer/type@^3.0.5": - version "3.0.5" - resolved "https://registry.npmjs.org/@inquirer/type/-/type-3.0.5.tgz" - integrity sha512-ZJpeIYYueOz/i/ONzrfof8g89kNdO2hjGuvULROo3O8rlB2CRtSseE5KeirnyE4t/thAn/EwvS/vuQeJCn+NZg== +"@inquirer/input@^4.1.9": + version "4.1.9" + resolved "https://registry.npmjs.org/@inquirer/input/-/input-4.1.9.tgz" + integrity sha512-mshNG24Ij5KqsQtOZMgj5TwEjIf+F2HOESk6bjMwGWgcH5UBe8UoljwzNFHqdMbGYbgAf6v2wU/X9CAdKJzgOA== + dependencies: + "@inquirer/core" "^10.1.10" + "@inquirer/type" "^3.0.6" + +"@inquirer/number@^3.0.12": + version "3.0.12" + resolved "https://registry.npmjs.org/@inquirer/number/-/number-3.0.12.tgz" + integrity sha512-7HRFHxbPCA4e4jMxTQglHJwP+v/kpFsCf2szzfBHy98Wlc3L08HL76UDiA87TOdX5fwj2HMOLWqRWv9Pnn+Z5Q== + dependencies: + "@inquirer/core" "^10.1.10" + "@inquirer/type" "^3.0.6" + +"@inquirer/password@^4.0.12": + version "4.0.12" + resolved "https://registry.npmjs.org/@inquirer/password/-/password-4.0.12.tgz" + integrity sha512-FlOB0zvuELPEbnBYiPaOdJIaDzb2PmJ7ghi/SVwIHDDSQ2K4opGBkF+5kXOg6ucrtSUQdLhVVY5tycH0j0l+0g== + dependencies: + "@inquirer/core" "^10.1.10" + "@inquirer/type" "^3.0.6" + ansi-escapes "^4.3.2" + +"@inquirer/prompts@^7.4.1": + version "7.4.1" + resolved "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.4.1.tgz" + integrity sha512-UlmM5FVOZF0gpoe1PT/jN4vk8JmpIWBlMvTL8M+hlvPmzN89K6z03+IFmyeu/oFCenwdwHDr2gky7nIGSEVvlA== + dependencies: + "@inquirer/checkbox" "^4.1.5" + "@inquirer/confirm" "^5.1.9" + "@inquirer/editor" "^4.2.10" + "@inquirer/expand" "^4.0.12" + "@inquirer/input" "^4.1.9" + "@inquirer/number" "^3.0.12" + "@inquirer/password" "^4.0.12" + "@inquirer/rawlist" "^4.0.12" + "@inquirer/search" "^3.0.12" + "@inquirer/select" "^4.1.1" + +"@inquirer/rawlist@^4.0.12": + version "4.0.12" + resolved "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.0.12.tgz" + integrity sha512-wNPJZy8Oc7RyGISPxp9/MpTOqX8lr0r+lCCWm7hQra+MDtYRgINv1hxw7R+vKP71Bu/3LszabxOodfV/uTfsaA== + dependencies: + "@inquirer/core" "^10.1.10" + "@inquirer/type" "^3.0.6" + yoctocolors-cjs "^2.1.2" + +"@inquirer/search@^3.0.12": + version "3.0.12" + resolved "https://registry.npmjs.org/@inquirer/search/-/search-3.0.12.tgz" + integrity sha512-H/kDJA3kNlnNIjB8YsaXoQI0Qccgf0Na14K1h8ExWhNmUg2E941dyFPrZeugihEa9AZNW5NdsD/NcvUME83OPQ== + dependencies: + "@inquirer/core" "^10.1.10" + "@inquirer/figures" "^1.0.11" + "@inquirer/type" "^3.0.6" + yoctocolors-cjs "^2.1.2" + +"@inquirer/select@^4.1.1": + version "4.1.1" + resolved "https://registry.npmjs.org/@inquirer/select/-/select-4.1.1.tgz" + integrity sha512-IUXzzTKVdiVNMA+2yUvPxWsSgOG4kfX93jOM4Zb5FgujeInotv5SPIJVeXQ+fO4xu7tW8VowFhdG5JRmmCyQ1Q== + dependencies: + "@inquirer/core" "^10.1.10" + "@inquirer/figures" "^1.0.11" + "@inquirer/type" "^3.0.6" + ansi-escapes "^4.3.2" + yoctocolors-cjs "^2.1.2" + +"@inquirer/type@^3.0.6": + version "3.0.6" + resolved "https://registry.npmjs.org/@inquirer/type/-/type-3.0.6.tgz" + integrity sha512-/mKVCtVpyBu3IDarv0G+59KC4stsD5mDsGpYh+GKs1NZT88Jh52+cuoA1AtLk2Q0r/quNl+1cSUyLRHBFeD0XA== "@isaacs/cliui@^8.0.2": version "8.0.2" @@ -860,11 +964,6 @@ resolved "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz" integrity sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg== -"@pkgjs/parseargs@^0.11.0": - version "0.11.0" - resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz" - integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== - "@sinclair/typebox@^0.27.8": version "0.27.8" resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz" @@ -934,10 +1033,10 @@ resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz" integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== -"@types/fs-extra@^11.0.2": - version "11.0.2" - resolved "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.2.tgz" - integrity sha512-c0hrgAOVYr21EX8J0jBMXGLMgJqVf/v6yxi0dLaJboW9aQPh16Id+z6w2Tx1hm+piJOLv8xPfVKZCLfjPw/IMQ== +"@types/fs-extra@^11.0.4": + version "11.0.4" + resolved "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz" + integrity sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ== dependencies: "@types/jsonfile" "*" "@types/node" "*" @@ -982,9 +1081,9 @@ integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== "@types/jsonfile@*": - version "6.1.1" - resolved "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.1.tgz" - integrity sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png== + version "6.1.4" + resolved "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz" + integrity sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ== dependencies: "@types/node" "*" @@ -993,11 +1092,6 @@ resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.198.tgz" integrity sha512-trNJ/vtMZYMLhfN45uLq4ShQSw0/S7xCTLLVM+WM1rmFpba/VS42jVUgaO3w/NOLiWR/09lnYk0yMaA/atdIsg== -"@types/luxon@^3.3.2": - version "3.3.2" - resolved "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.2.tgz" - integrity sha512-l5cpE57br4BIjK+9BSkFBOsWtwv6J9bJpC7gdXIzZyI0vuKvNTk0wZZrkQxMGsUAuGW9+WMNWF2IJMD7br2yeQ== - "@types/node@*", "@types/node@>=18": version "22.13.10" resolved "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz" @@ -1156,16 +1250,9 @@ ansi-regex@^5.0.1: integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-regex@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz" - integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== - -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" + version "6.1.0" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz" + integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" @@ -1379,15 +1466,6 @@ caniuse-lite@^1.0.30001688: resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz" integrity sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w== -chalk@^2.3.2: - version "2.4.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.1, chalk@^4.1.2: version "4.1.2" resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" @@ -1401,6 +1479,11 @@ char-regex@^1.0.2: resolved "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + ci-info@^3.2.0: version "3.9.0" resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz" @@ -1442,13 +1525,6 @@ collect-v8-coverage@^1.0.0: resolved "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz" integrity sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q== -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - color-convert@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" @@ -1461,11 +1537,6 @@ color-name@~1.1.4: resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= - colors@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz" @@ -1503,6 +1574,11 @@ concat-map@0.0.1: resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +consola@^3.4.2: + version "3.4.2" + resolved "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz" + integrity sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA== + console-table-printer@^2.12.1: version "2.12.1" resolved "https://registry.npmjs.org/console-table-printer/-/console-table-printer-2.12.1.tgz" @@ -1547,6 +1623,11 @@ dateformat@^5.0.3: resolved "https://registry.npmjs.org/dateformat/-/dateformat-5.0.3.tgz" integrity sha512-Kvr6HmPXUMerlLcLF+Pwq3K7apHpYmGDVqrxcDasBg86UcKeTSNWbEzU8bwdXnxnR44FtMhJAxI4Bov6Y/KUfA== +dayjs@^1.11.13: + version "1.11.13" + resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz" + integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== + debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.4.0" resolved "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz" @@ -1659,11 +1740,6 @@ escalade@^3.1.1, escalade@^3.2.0: resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz" integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - escape-string-regexp@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz" @@ -1802,6 +1878,15 @@ expect@^29.0.0, expect@^29.7.0: jest-message-util "^29.7.0" jest-util "^29.7.0" +external-editor@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + fast-content-type-parse@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz" @@ -1847,13 +1932,6 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" -figures@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz" - integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI= - dependencies: - escape-string-regexp "^1.0.5" - file-entry-cache@^8.0.0: version "8.0.0" resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz" @@ -1875,13 +1953,6 @@ fill-range@^7.1.1: dependencies: to-regex-range "^5.0.1" -find-up@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz" - integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= - dependencies: - locate-path "^2.0.0" - find-up@^4.0.0: version "4.1.0" resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz" @@ -1941,10 +2012,10 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -fs-extra@^11.1.1: - version "11.1.1" - resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz" - integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ== +fs-extra@^11.3.0: + version "11.3.0" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz" + integrity sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew== dependencies: graceful-fs "^4.2.0" jsonfile "^6.0.1" @@ -2011,21 +2082,10 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" -glob@^10.3.4: - version "10.3.4" - resolved "https://registry.npmjs.org/glob/-/glob-10.3.4.tgz" - integrity sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ== - dependencies: - foreground-child "^3.1.0" - jackspeak "^2.0.3" - minimatch "^9.0.1" - minipass "^5.0.0 || ^6.0.2 || ^7.0.0" - path-scurry "^1.10.1" - -glob@^11.0.0: - version "11.0.0" - resolved "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz" - integrity sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g== +glob@^11.0.0, glob@^11.0.1, glob@^11.0.2: + version "11.0.2" + resolved "https://registry.npmjs.org/glob/-/glob-11.0.2.tgz" + integrity sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ== dependencies: foreground-child "^3.1.0" jackspeak "^4.0.1" @@ -2073,7 +2133,7 @@ globals@^16.0.0: resolved "https://registry.npmjs.org/globals/-/globals-16.0.0.tgz" integrity sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A== -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.9: +graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.9: version "4.2.9" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz" integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== @@ -2088,11 +2148,6 @@ graphql@^16.8.1: resolved "https://registry.npmjs.org/graphql/-/graphql-16.10.0.tgz" integrity sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ== -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= - has-flag@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" @@ -2120,6 +2175,13 @@ human-signals@^2.1.0: resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== +iconv-lite@^0.4.24: + version "0.4.24" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + ignore@^5.2.0, ignore@^5.3.1: version "5.3.2" resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz" @@ -2356,19 +2418,10 @@ istanbul-reports@^3.1.3: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -jackspeak@^2.0.3: - version "2.3.3" - resolved "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.3.tgz" - integrity sha512-R2bUw+kVZFS/h1AZqBKrSgDmdmjApzgY0AlCPumopFiAlbUxE2gf+SCuBzQ0cP5hHmUmFYF5yw55T97Th5Kstg== - dependencies: - "@isaacs/cliui" "^8.0.2" - optionalDependencies: - "@pkgjs/parseargs" "^0.11.0" - jackspeak@^4.0.1: - version "4.0.2" - resolved "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz" - integrity sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw== + version "4.1.0" + resolved "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz" + integrity sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw== dependencies: "@isaacs/cliui" "^8.0.2" @@ -2780,11 +2833,6 @@ json-buffer@3.0.1: resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz" integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== -json-parse-better-errors@^1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz" - integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== - json-parse-even-better-errors@^2.3.0: version "2.3.1" resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" @@ -2844,24 +2892,6 @@ lines-and-columns@^1.1.6: resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== -load-json-file@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz" - integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs= - dependencies: - graceful-fs "^4.1.2" - parse-json "^4.0.0" - pify "^3.0.0" - strip-bom "^3.0.0" - -locate-path@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz" - integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= - dependencies: - p-locate "^2.0.0" - path-exists "^3.0.0" - locate-path@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz" @@ -2892,9 +2922,9 @@ lodash@^4.17.21: integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== lru-cache@^11.0.0: - version "11.0.2" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz" - integrity sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA== + version "11.1.0" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz" + integrity sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A== lru-cache@^5.1.1: version "5.1.1" @@ -2903,16 +2933,6 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" -"lru-cache@^9.1.1 || ^10.0.0": - version "10.0.1" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz" - integrity sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g== - -luxon@^3.4.3: - version "3.4.3" - resolved "https://registry.npmjs.org/luxon/-/luxon-3.4.3.tgz" - integrity sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg== - m3u-linter@^0.4.2: version "0.4.2" resolved "https://registry.npmjs.org/m3u-linter/-/m3u-linter-0.4.2.tgz" @@ -3005,13 +3025,6 @@ minimatch@^5.0.1: dependencies: brace-expansion "^2.0.1" -minimatch@^9.0.1: - version "9.0.3" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz" - integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== - dependencies: - brace-expansion "^2.0.1" - minimatch@^9.0.4: version "9.0.5" resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz" @@ -3019,7 +3032,7 @@ minimatch@^9.0.4: dependencies: brace-expansion "^2.0.1" -"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: +minipass@^7.1.2: version "7.1.2" resolved "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz" integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== @@ -3063,15 +3076,15 @@ natural-compare@^1.4.0: resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -natural-orderby@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/natural-orderby/-/natural-orderby-3.0.2.tgz" - integrity sha512-x7ZdOwBxZCEm9MM7+eQCjkrNLrW3rkBKNHVr78zbtqnMGVNlnDi6C/eUEYgxHNrcbu0ymvjzcwIL/6H1iHri9g== +natural-orderby@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/natural-orderby/-/natural-orderby-5.0.0.tgz" + integrity sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg== -node-gzip@^1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/node-gzip/-/node-gzip-1.1.2.tgz" - integrity sha512-ZB6zWpfZHGtxZnPMrJSKHVPrRjURoUzaDbLFj3VO70mpLTW5np96vXyHwft4Id0o+PYIzgDkBUjIzaNHhQ8srw== +node-cleanup@^2.1.2: + version "2.1.2" + resolved "https://registry.npmjs.org/node-cleanup/-/node-cleanup-2.1.2.tgz" + integrity sha512-qN8v/s2PAJwGUtr1/hYTpNKlD6Y9rc4p8KSmJXyGdYGZsDGKXrGThikLFP9OCHFeLeEpQzPwiAtdIvBLqm//Hw== node-int64@^0.4.0: version "0.4.0" @@ -3141,18 +3154,16 @@ optionator@^0.9.3: type-check "^0.4.0" word-wrap "^1.2.5" +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz" + integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== + outvariant@^1.4.0, outvariant@^1.4.3: version "1.4.3" resolved "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz" integrity sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA== -p-limit@^1.1.0: - version "1.3.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz" - integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== - dependencies: - p-try "^1.0.0" - p-limit@^2.2.0: version "2.3.0" resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" @@ -3167,13 +3178,6 @@ p-limit@^3.0.2, p-limit@^3.1.0: dependencies: yocto-queue "^0.1.0" -p-locate@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz" - integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= - dependencies: - p-limit "^1.1.0" - p-locate@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz" @@ -3188,11 +3192,6 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" -p-try@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz" - integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= - p-try@^2.0.0: version "2.2.0" resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz" @@ -3203,6 +3202,11 @@ package-json-from-dist@^1.0.0: resolved "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz" integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== +pako@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz" + integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" @@ -3210,14 +3214,6 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-json@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz" - integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= - dependencies: - error-ex "^1.3.1" - json-parse-better-errors "^1.0.1" - parse-json@^5.2.0: version "5.2.0" resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz" @@ -3228,11 +3224,6 @@ parse-json@^5.2.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz" - integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= - path-exists@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" @@ -3253,14 +3244,6 @@ path-parse@^1.0.7: resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-scurry@^1.10.1: - version "1.10.1" - resolved "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz" - integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ== - dependencies: - lru-cache "^9.1.1 || ^10.0.0" - minipass "^5.0.0 || ^6.0.2 || ^7.0.0" - path-scurry@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz" @@ -3284,24 +3267,11 @@ picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1: resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -pify@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz" - integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= - pirates@^4.0.4: version "4.0.6" resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz" integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== -pkg-conf@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/pkg-conf/-/pkg-conf-2.1.0.tgz" - integrity sha1-ISZRTKbyq/69FoWW3xi6V4Z/AFg= - dependencies: - find-up "^2.0.0" - load-json-file "^4.0.0" - pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz" @@ -3436,10 +3406,10 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -run-script-os@^1.1.6: - version "1.1.6" - resolved "https://registry.npmjs.org/run-script-os/-/run-script-os-1.1.6.tgz" - integrity sha512-ql6P2LzhBTTDfzKts+Qo4H94VUKpxKDFz6QxxwaUZN0mwvi7L3lpOI7BqPCq7lgDh3XLl0dpeXwfcVIitlrYrw== +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== semver@^6.3.0: version "6.3.1" @@ -3483,15 +3453,6 @@ signal-exit@^4.1.0: resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== -signale@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/signale/-/signale-1.4.0.tgz" - integrity sha512-iuh+gPf28RkltuJC7W5MRi6XAjTDCAPC/prJUpQoG4vIP3MJZ+GTydVnodXA7pwvTKb2cA0m9OFZW/cdWy/I/w== - dependencies: - chalk "^2.3.2" - figures "^2.0.0" - pkg-conf "^2.1.0" - simple-wcswidth@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/simple-wcswidth/-/simple-wcswidth-1.0.1.tgz" @@ -3625,11 +3586,6 @@ strip-ansi@^7.0.1: dependencies: ansi-regex "^6.0.1" -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" - integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= - strip-bom@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz" @@ -3645,13 +3601,6 @@ strip-json-comments@^3.1.1: resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - supports-color@^7.1.0: version "7.2.0" resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" @@ -3680,6 +3629,18 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" +timer-node@^5.0.9: + version "5.0.9" + resolved "https://registry.npmjs.org/timer-node/-/timer-node-5.0.9.tgz" + integrity sha512-zXxCE/5/YDi0hY9pygqgRqjRbrFRzigYxOudG0I3syaqAAmX9/w9sxex1bNFCN6c1S66RwPtEIJv65dN+1psew== + +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + tmpl@1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz" @@ -3702,13 +3663,6 @@ tough-cookie@^4.1.4: universalify "^0.2.0" url-parse "^1.5.3" -transliteration@^2.3.5: - version "2.3.5" - resolved "https://registry.npmjs.org/transliteration/-/transliteration-2.3.5.tgz" - integrity sha512-HAGI4Lq4Q9dZ3Utu2phaWgtm3vB6PkLUFqWAScg/UW+1eZ/Tg6Exo4oC0/3VUol/w4BlefLhUUSVBr/9/ZGQOw== - dependencies: - yargs "^17.5.1" - ts-api-utils@^1.3.0: version "1.4.3" resolved "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz" @@ -3782,9 +3736,9 @@ universalify@^0.2.0: integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + version "2.0.1" + resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== update-browserslist-db@^1.1.1: version "1.1.1" @@ -3911,7 +3865,7 @@ yargs-parser@^21.1.1: resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== -yargs@^17.3.1, yargs@^17.5.1, yargs@^17.7.2: +yargs@^17.3.1, yargs@^17.7.2: version "17.7.2" resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==