├── VERSION ├── .github ├── 1.png ├── 2.png ├── 3.png └── workflows │ └── docker-publish.yml ├── PlutoIPTV ├── .gitignore ├── README.md ├── package.json ├── LICENSE ├── yarn.lock └── index.js ├── script └── release ├── Dockerfile ├── index.html ├── LICENSE.md ├── entrypoint.sh └── README.md /VERSION: -------------------------------------------------------------------------------- 1 | 1.2.10 2 | -------------------------------------------------------------------------------- /.github/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iptv-restream/pluto-for-channels/HEAD/.github/1.png -------------------------------------------------------------------------------- /.github/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iptv-restream/pluto-for-channels/HEAD/.github/2.png -------------------------------------------------------------------------------- /.github/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iptv-restream/pluto-for-channels/HEAD/.github/3.png -------------------------------------------------------------------------------- /PlutoIPTV/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | cache.json 3 | *playlist.m3u8 4 | *playlist.m3u 5 | *epg.xml 6 | proxy/index.js 7 | -------------------------------------------------------------------------------- /PlutoIPTV/README.md: -------------------------------------------------------------------------------- 1 | # PlutoIPTV 2 | 3 | Grab EPG & M3U from Pluto.tv 4 | 5 | Based on https://github.com/TylerB260/PlutoXML 6 | 7 | ## Usage 8 | 9 | Run 10 | 11 | ```bash 12 | $ npx pluto-iptv 13 | ``` 14 | 15 | This will create an `epg.xml` file and a `playlist.m3u` file 16 | -------------------------------------------------------------------------------- /script/release: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [[ $# -eq 0 ]] ; then 4 | echo 'Please include a version number: ie, 1.0.22' 5 | exit 0 6 | fi 7 | 8 | cat < VERSION 9 | $1 10 | EOF 11 | 12 | echo "Creating release and pushing it to GitHub..." 13 | git commit -am "Bump to $1" 14 | git push 15 | git tag $1 16 | git push --tags 17 | 18 | echo "Opening release for editing..." 19 | open "https://github.com/maddox/pluto-for-channels/releases/edit/$1" 20 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.18-alpine 2 | LABEL maintainer="Jon Maddox " 3 | 4 | WORKDIR /usr/src/app 5 | ADD VERSION /usr/src/app/VERSION 6 | ADD entrypoint.sh /usr/src/app/entrypoint.sh 7 | ADD index.html /usr/src/app/index.html 8 | ADD PlutoIPTV/* /usr/src/app/ 9 | 10 | # Install nvm with node and npm 11 | RUN apk add --no-cache --update-cache libuv nodejs-current npm yarn \ 12 | && echo "NodeJS Version:" "$(node -v)" \ 13 | && echo "NPM Version:" "$(npm -v)" \ 14 | && echo "Yarn Version:" "$(yarn -v)" \ 15 | && yarn --production --no-progress \ 16 | && rm -rf /tmp/* 17 | 18 | ENTRYPOINT ["/usr/src/app/entrypoint.sh"] 19 | -------------------------------------------------------------------------------- /PlutoIPTV/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pluto-iptv", 3 | "version": "1.0.6", 4 | "description": "pluto.tv to XMLTV & M3U converter", 5 | "main": "index.js", 6 | "dependencies": { 7 | "fs-extra": "^9.0.0", 8 | "jsontoxml": "^1.0.1", 9 | "moment": "^2.24.0", 10 | "request": "^2.88.2", 11 | "uuid": "^7.0.2" 12 | }, 13 | "devDependencies": {}, 14 | "scripts": { 15 | "test": "echo \"Error: no test specified\" && exit 1" 16 | }, 17 | "bin": { 18 | "pluto-iptv": "./index.js" 19 | }, 20 | "repository": "git+https://github.com/evoactivity/PlutoIPTV.git", 21 | "author": "Liam Potter ", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/evoactivity/PlutoIPTV/issues" 25 | }, 26 | "homepage": "https://github.com/evoactivity/PlutoIPTV#readme" 27 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Pluto for Channels 7 | 8 | 13 | 14 | 15 |
16 |
17 |

18 | Pluto for Channels 19 | vVERSION 20 | UPDATE_AVAILABLE 21 |

22 |

23 | Last Updated: LAST_RAN 24 |

25 | 29 | LINKED_VERSIONS 30 |
31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2021 Jon Maddox 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /PlutoIPTV/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Liam Potter 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: docker 2 | on: 3 | push: 4 | jobs: 5 | docker: 6 | name: docker 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: checkout 10 | uses: actions/checkout@v2 11 | 12 | - name: Set up QEMU 13 | uses: docker/setup-qemu-action@v1 14 | 15 | - name: Docker meta 16 | id: docker_meta 17 | uses: crazy-max/ghaction-docker-meta@v1 18 | with: 19 | images: jonmaddox/pluto-for-channels 20 | 21 | - name: Set up Docker Buildx 22 | uses: docker/setup-buildx-action@v1 23 | 24 | - name: Login to DockerHub 25 | uses: docker/login-action@v1 26 | with: 27 | username: ${{ secrets.DOCKER_USERNAME }} 28 | password: ${{ secrets.DOCKER_PASSWORD }} 29 | 30 | - name: Build and push 31 | uses: docker/build-push-action@v2 32 | with: 33 | context: . 34 | file: ./Dockerfile 35 | platforms: linux/amd64,linux/arm64,linux/arm/v6,linux/arm/v7,linux/386 36 | push: true 37 | tags: | 38 | ${{ steps.docker_meta.outputs.tags }} 39 | ${{ 'jonmaddox/pluto-for-channels:latest' }} 40 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd /usr/src/app 4 | 5 | nginx 6 | 7 | NGINX_ROOT=/usr/share/nginx/html 8 | 9 | get_latest_release() { 10 | curl --silent "https://api.github.com/repos/maddox/pluto-for-channels/releases/latest" | 11 | grep '"tag_name":' | 12 | sed -E 's/.*"([^"]+)".*/\1/' 13 | } 14 | 15 | while : 16 | do 17 | node index.js $VERSIONS 18 | 19 | CURRENT_VERSION=`cat VERSION` 20 | LATEST_VERSION=`get_latest_release` 21 | UPDATE_AVAILABLE="" 22 | LAST_RAN=`date` 23 | 24 | if [ "$CURRENT_VERSION" != "$LATEST_VERSION" ]; then 25 | UPDATE_AVAILABLE="\\UPDATE AVAILABLE\: $LATEST_VERSION\<\/span\>\<\/a\>" 26 | fi 27 | 28 | LINKED_VERSIONS="" 29 | 30 | for i in $(echo $VERSIONS | sed "s/,/ /g") 31 | do 32 | LINKED_VERSIONS="$LINKED_VERSIONS \\\$i Playlist\<\/a\>\<\/li\>\\$i EPG\<\/a\>\<\/li\>\<\/ul\>" 33 | done 34 | 35 | echo $LINKED_VERSIONS 36 | 37 | sed -e "s/LAST_RAN/$LAST_RAN/g" \ 38 | -e "s/LINKED_VERSIONS/$LINKED_VERSIONS/g" \ 39 | -e "s/VERSION/$CURRENT_VERSION/g" \ 40 | -e "s/UPDATE_AVAILABLE/$UPDATE_AVAILABLE/g" \ 41 | index.html > "$NGINX_ROOT/index.html" 42 | 43 | mv *playlist.m3u "$NGINX_ROOT" 44 | mv *epg.xml "$NGINX_ROOT" 45 | echo "Last ran: $LAST_RAN" 46 | sleep 10800 # run every 3 hours 47 | done 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pluto for Channels 2 | 3 | This simple Docker image will generate an M3U playlist and EPG optimized for use in [Channels](https://getchannels.com) and expose them over HTTP. 4 | 5 | [Channels](https://getchannels.com) supports [custom channels](https://getchannels.com/docs/channels-dvr-server/how-to/custom-channels/) by utilizing streaming sources via M3U playlists. 6 | 7 | [Channels](https://getchannels.com) allows for [additional extended metadata tags](https://getchannels.com/docs/channels-dvr-server/how-to/custom-channels/#channels-extensions) in M3U playlists that allow you to give it extra information and art to make the experience better. This project adds those extra tags to make things look great in Channels. 8 | 9 | ## Set Up 10 | 11 | Running the container is easy. Fire up the container as usual. You can set which port it runs on. 12 | 13 | docker run -d --restart unless-stopped --name pluto-for-channels -p 8080:80 jonmaddox/pluto-for-channels 14 | 15 | You can retrieve the playlist and EPG via the status page. 16 | 17 | http://127.0.0.1:8080 18 | 19 | ### Optionally have multiple feeds generated 20 | 21 | By using the `VERSIONS` env var when starting the docker container, you can tell it to create multiple feeds that can be used elsewhere. 22 | 23 | Simpley provide a comma separated list of words without spaces with the `VERSIONS` env var. 24 | 25 | docker run -d --restart unless-stopped --name pluto-for-channels -p 8080:80 -e VERSIONS=Dad,Bob,Joe jonmaddox/pluto-for-channels 26 | 27 | 28 | ## Add Source to Channels 29 | 30 | Once you have your Pluto M3U and EPG XML available, you can use it to [custom channels](https://getchannels.com/docs/channels-dvr-server/how-to/custom-channels/) channels in the [Channels](https://getchannels.com) app. 31 | 32 | Add a new source in Channels DVR Server and choose `M3U Playlist`. Fill out the form using your new playlist URL. 33 | 34 | 35 | 36 | Next, set the provider for your new source and choose custom URL. 37 | 38 | 39 | 40 | Finally, enter your EPG xml url and set it to refresh every 6 hours. 41 | 42 | 43 | -------------------------------------------------------------------------------- /PlutoIPTV/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | ajv@^6.5.5: 6 | version "6.12.0" 7 | resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.0.tgz#06d60b96d87b8454a5adaba86e7854da629db4b7" 8 | integrity sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw== 9 | dependencies: 10 | fast-deep-equal "^3.1.1" 11 | fast-json-stable-stringify "^2.0.0" 12 | json-schema-traverse "^0.4.1" 13 | uri-js "^4.2.2" 14 | 15 | asn1@~0.2.3: 16 | version "0.2.4" 17 | resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" 18 | integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== 19 | dependencies: 20 | safer-buffer "~2.1.0" 21 | 22 | assert-plus@1.0.0, assert-plus@^1.0.0: 23 | version "1.0.0" 24 | resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" 25 | integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= 26 | 27 | asynckit@^0.4.0: 28 | version "0.4.0" 29 | resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" 30 | integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= 31 | 32 | at-least-node@^1.0.0: 33 | version "1.0.0" 34 | resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" 35 | integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== 36 | 37 | aws-sign2@~0.7.0: 38 | version "0.7.0" 39 | resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" 40 | integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= 41 | 42 | aws4@^1.8.0: 43 | version "1.9.1" 44 | resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e" 45 | integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug== 46 | 47 | bcrypt-pbkdf@^1.0.0: 48 | version "1.0.2" 49 | resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" 50 | integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= 51 | dependencies: 52 | tweetnacl "^0.14.3" 53 | 54 | caseless@~0.12.0: 55 | version "0.12.0" 56 | resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" 57 | integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= 58 | 59 | combined-stream@^1.0.6, combined-stream@~1.0.6: 60 | version "1.0.8" 61 | resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" 62 | integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== 63 | dependencies: 64 | delayed-stream "~1.0.0" 65 | 66 | core-util-is@1.0.2: 67 | version "1.0.2" 68 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 69 | integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= 70 | 71 | dashdash@^1.12.0: 72 | version "1.14.1" 73 | resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" 74 | integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= 75 | dependencies: 76 | assert-plus "^1.0.0" 77 | 78 | delayed-stream@~1.0.0: 79 | version "1.0.0" 80 | resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" 81 | integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= 82 | 83 | ecc-jsbn@~0.1.1: 84 | version "0.1.2" 85 | resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" 86 | integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= 87 | dependencies: 88 | jsbn "~0.1.0" 89 | safer-buffer "^2.1.0" 90 | 91 | extend@~3.0.2: 92 | version "3.0.2" 93 | resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" 94 | integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== 95 | 96 | extsprintf@1.3.0: 97 | version "1.3.0" 98 | resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" 99 | integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= 100 | 101 | extsprintf@^1.2.0: 102 | version "1.4.0" 103 | resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" 104 | integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= 105 | 106 | fast-deep-equal@^3.1.1: 107 | version "3.1.1" 108 | resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" 109 | integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== 110 | 111 | fast-json-stable-stringify@^2.0.0: 112 | version "2.1.0" 113 | resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" 114 | integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== 115 | 116 | forever-agent@~0.6.1: 117 | version "0.6.1" 118 | resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" 119 | integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= 120 | 121 | form-data@~2.3.2: 122 | version "2.3.3" 123 | resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" 124 | integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== 125 | dependencies: 126 | asynckit "^0.4.0" 127 | combined-stream "^1.0.6" 128 | mime-types "^2.1.12" 129 | 130 | fs-extra@^9.0.0: 131 | version "9.0.0" 132 | resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.0.tgz#b6afc31036e247b2466dc99c29ae797d5d4580a3" 133 | integrity sha512-pmEYSk3vYsG/bF651KPUXZ+hvjpgWYw/Gc7W9NFUe3ZVLczKKWIij3IKpOrQcdw4TILtibFslZ0UmR8Vvzig4g== 134 | dependencies: 135 | at-least-node "^1.0.0" 136 | graceful-fs "^4.2.0" 137 | jsonfile "^6.0.1" 138 | universalify "^1.0.0" 139 | 140 | getpass@^0.1.1: 141 | version "0.1.7" 142 | resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" 143 | integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= 144 | dependencies: 145 | assert-plus "^1.0.0" 146 | 147 | graceful-fs@^4.1.6, graceful-fs@^4.2.0: 148 | version "4.2.3" 149 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" 150 | integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== 151 | 152 | har-schema@^2.0.0: 153 | version "2.0.0" 154 | resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" 155 | integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= 156 | 157 | har-validator@~5.1.3: 158 | version "5.1.3" 159 | resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" 160 | integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== 161 | dependencies: 162 | ajv "^6.5.5" 163 | har-schema "^2.0.0" 164 | 165 | http-signature@~1.2.0: 166 | version "1.2.0" 167 | resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" 168 | integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= 169 | dependencies: 170 | assert-plus "^1.0.0" 171 | jsprim "^1.2.2" 172 | sshpk "^1.7.0" 173 | 174 | is-typedarray@~1.0.0: 175 | version "1.0.0" 176 | resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" 177 | integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= 178 | 179 | isstream@~0.1.2: 180 | version "0.1.2" 181 | resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" 182 | integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= 183 | 184 | jsbn@~0.1.0: 185 | version "0.1.1" 186 | resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" 187 | integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= 188 | 189 | json-schema-traverse@^0.4.1: 190 | version "0.4.1" 191 | resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" 192 | integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== 193 | 194 | json-schema@0.2.3: 195 | version "0.2.3" 196 | resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" 197 | integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= 198 | 199 | json-stringify-safe@~5.0.1: 200 | version "5.0.1" 201 | resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" 202 | integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= 203 | 204 | jsonfile@^6.0.1: 205 | version "6.0.1" 206 | resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.0.1.tgz#98966cba214378c8c84b82e085907b40bf614179" 207 | integrity sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg== 208 | dependencies: 209 | universalify "^1.0.0" 210 | optionalDependencies: 211 | graceful-fs "^4.1.6" 212 | 213 | jsontoxml@^1.0.1: 214 | version "1.0.1" 215 | resolved "https://registry.yarnpkg.com/jsontoxml/-/jsontoxml-1.0.1.tgz#07fff7f6bfbfa1097d779aec7f041b5046075e70" 216 | integrity sha512-dtKGq0K8EWQBRqcAaePSgKR4Hyjfsz/LkurHSV3Cxk4H+h2fWDeaN2jzABz+ZmOJylgXS7FGeWmbZ6jgYUMdJQ== 217 | 218 | jsprim@^1.2.2: 219 | version "1.4.1" 220 | resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" 221 | integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= 222 | dependencies: 223 | assert-plus "1.0.0" 224 | extsprintf "1.3.0" 225 | json-schema "0.2.3" 226 | verror "1.10.0" 227 | 228 | mime-db@1.43.0: 229 | version "1.43.0" 230 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" 231 | integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ== 232 | 233 | mime-types@^2.1.12, mime-types@~2.1.19: 234 | version "2.1.26" 235 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" 236 | integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ== 237 | dependencies: 238 | mime-db "1.43.0" 239 | 240 | moment@^2.24.0: 241 | version "2.24.0" 242 | resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" 243 | integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== 244 | 245 | oauth-sign@~0.9.0: 246 | version "0.9.0" 247 | resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" 248 | integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== 249 | 250 | performance-now@^2.1.0: 251 | version "2.1.0" 252 | resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" 253 | integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= 254 | 255 | psl@^1.1.28: 256 | version "1.7.0" 257 | resolved "https://registry.yarnpkg.com/psl/-/psl-1.7.0.tgz#f1c4c47a8ef97167dea5d6bbf4816d736e884a3c" 258 | integrity sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ== 259 | 260 | punycode@^2.1.0, punycode@^2.1.1: 261 | version "2.1.1" 262 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" 263 | integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== 264 | 265 | qs@~6.5.2: 266 | version "6.5.2" 267 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" 268 | integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== 269 | 270 | request@^2.88.2: 271 | version "2.88.2" 272 | resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" 273 | integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== 274 | dependencies: 275 | aws-sign2 "~0.7.0" 276 | aws4 "^1.8.0" 277 | caseless "~0.12.0" 278 | combined-stream "~1.0.6" 279 | extend "~3.0.2" 280 | forever-agent "~0.6.1" 281 | form-data "~2.3.2" 282 | har-validator "~5.1.3" 283 | http-signature "~1.2.0" 284 | is-typedarray "~1.0.0" 285 | isstream "~0.1.2" 286 | json-stringify-safe "~5.0.1" 287 | mime-types "~2.1.19" 288 | oauth-sign "~0.9.0" 289 | performance-now "^2.1.0" 290 | qs "~6.5.2" 291 | safe-buffer "^5.1.2" 292 | tough-cookie "~2.5.0" 293 | tunnel-agent "^0.6.0" 294 | uuid "^3.3.2" 295 | 296 | safe-buffer@^5.0.1, safe-buffer@^5.1.2: 297 | version "5.2.0" 298 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" 299 | integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== 300 | 301 | safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: 302 | version "2.1.2" 303 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 304 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 305 | 306 | sshpk@^1.7.0: 307 | version "1.16.1" 308 | resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" 309 | integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== 310 | dependencies: 311 | asn1 "~0.2.3" 312 | assert-plus "^1.0.0" 313 | bcrypt-pbkdf "^1.0.0" 314 | dashdash "^1.12.0" 315 | ecc-jsbn "~0.1.1" 316 | getpass "^0.1.1" 317 | jsbn "~0.1.0" 318 | safer-buffer "^2.0.2" 319 | tweetnacl "~0.14.0" 320 | 321 | tough-cookie@~2.5.0: 322 | version "2.5.0" 323 | resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" 324 | integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== 325 | dependencies: 326 | psl "^1.1.28" 327 | punycode "^2.1.1" 328 | 329 | tunnel-agent@^0.6.0: 330 | version "0.6.0" 331 | resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" 332 | integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= 333 | dependencies: 334 | safe-buffer "^5.0.1" 335 | 336 | tweetnacl@^0.14.3, tweetnacl@~0.14.0: 337 | version "0.14.5" 338 | resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" 339 | integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= 340 | 341 | universalify@^1.0.0: 342 | version "1.0.0" 343 | resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" 344 | integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== 345 | 346 | uri-js@^4.2.2: 347 | version "4.2.2" 348 | resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" 349 | integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== 350 | dependencies: 351 | punycode "^2.1.0" 352 | 353 | uuid@^3.3.2: 354 | version "3.4.0" 355 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" 356 | integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== 357 | 358 | uuid@^7.0.2: 359 | version "7.0.2" 360 | resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.2.tgz#7ff5c203467e91f5e0d85cfcbaaf7d2ebbca9be6" 361 | integrity sha512-vy9V/+pKG+5ZTYKf+VcphF5Oc6EFiu3W8Nv3P3zIh0EqVI80ZxOzuPfe9EHjkFNvf8+xuTHVeei4Drydlx4zjw== 362 | 363 | verror@1.10.0: 364 | version "1.10.0" 365 | resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" 366 | integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= 367 | dependencies: 368 | assert-plus "^1.0.0" 369 | core-util-is "1.0.2" 370 | extsprintf "^1.2.0" 371 | -------------------------------------------------------------------------------- /PlutoIPTV/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const request = require("request"); 4 | const j2x = require("jsontoxml"); 5 | const moment = require("moment"); 6 | const fs = require("fs-extra"); 7 | const uuid4 = require("uuid").v4; 8 | const uuid1 = require("uuid").v1; 9 | const url = require("url"); 10 | 11 | const conflictingChannels = ["cnn"]; 12 | 13 | const kidsGenres = [ 14 | "Kids", 15 | "Children & Family", 16 | "Kids' TV", 17 | "Cartoons", 18 | "Animals", 19 | "Family Animation", 20 | "Ages 2-4", 21 | "Ages 11-12", 22 | ]; 23 | const newsGenres = ["News + Opinion", "General News"]; 24 | const sportsGenres = [ 25 | "Sports", 26 | "Sports & Sports Highlights", 27 | "Sports Documentaries", 28 | ]; 29 | const dramaGenres = [ 30 | "Crime", 31 | "Action & Adventure", 32 | "Thrillers", 33 | "Romance", 34 | "Sci-Fi & Fantasy", 35 | "Teen Dramas", 36 | "Film Noir", 37 | "Romantic Comedies", 38 | "Indie Dramas", 39 | "Romance Classics", 40 | "Crime Action", 41 | "Action Sci-Fi & Fantasy", 42 | "Action Thrillers", 43 | "Crime Thrillers", 44 | "Political Thrillers", 45 | "Classic Thrillers", 46 | "Classic Dramas", 47 | "Sci-Fi Adventure", 48 | "Romantic Dramas", 49 | "Mystery", 50 | "Psychological Thrillers", 51 | "Foreign Classic Dramas", 52 | "Classic Westerns", 53 | "Westerns", 54 | "Sci-Fi Dramas", 55 | "Supernatural Thrillers", 56 | "Mobster", 57 | "Action Classics", 58 | "African-American Action", 59 | "Suspense", 60 | "Family Dramas", 61 | "Alien Sci-Fi", 62 | "Sci-Fi Cult Classics", 63 | ]; 64 | 65 | const movieGenres = [ 66 | [ 67 | ["Action"], 68 | [ 69 | "Action & Adventure", 70 | "Crime Action", 71 | "Action Sci-Fi & Fantasy", 72 | "Action Thrillers", 73 | "Action Classics", 74 | "African-American Action", 75 | ], 76 | ], 77 | [["Adventure"], ["Action & Adventure", "Sci-Fi Adventure"]], 78 | [["Crime"], ["Crime Action", "Crime Thrillers"]], 79 | [["Documentary"], ["Documentaries"]], 80 | [ 81 | ["Thriller"], 82 | [ 83 | "Thrillers", 84 | "Action Thrillers", 85 | "Crime Thrillers", 86 | "Political Thrillers", 87 | "Classic Thrillers", 88 | "Psychological Thrillers", 89 | "Supernatural Thrillers", 90 | ], 91 | ], 92 | [ 93 | ["Science fiction"], 94 | [ 95 | "Sci-Fi & Fantasy", 96 | "Action Sci-Fi & Fantasy", 97 | "Sci-Fi Adventure", 98 | "Sci-Fi Dramas", 99 | "Alien Sci-Fi", 100 | "Sci-Fi Cult Classics", 101 | ], 102 | ], 103 | [["Fantasy"], ["Sci-Fi & Fantasy", "Action Sci-Fi & Fantasy"]], 104 | [ 105 | ["Drama"], 106 | [ 107 | "Teen Dramas", 108 | "Indie Dramas", 109 | "Classic Dramas", 110 | "Romantic Dramas", 111 | "Sci-Fi Dramas", 112 | "Family Dramas", 113 | ], 114 | ], 115 | [["Romantic comedy"], ["Romantic Comedies"]], 116 | [["Romance"], ["Romance", "Romance Classics", "Romantic Dramas"]], 117 | [["Western"], ["Classic Westerns", "Westerns"]], 118 | [["Mystery"], ["Suspense"]], 119 | ]; 120 | 121 | const seriesGenres = [ 122 | [["Animated"], ["Family Animation", "Cartoons"]], 123 | [["Educational"], ["Education & Guidance", "Instructional & Educational"]], 124 | [["News"], ["News and Information", "General News"]], 125 | [["History"], ["History & Social Studies"]], 126 | [["Politics"], ["Politics"]], 127 | [ 128 | ["Action"], 129 | [ 130 | "Action & Adventure", 131 | "Action Classics", 132 | "Martial Arts", 133 | "Crime Action", 134 | "Family Adventures", 135 | ], 136 | ], 137 | [["Adventure"], ["Action & Adventure", "Adventures", "Sci-Fi Adventure"]], 138 | [ 139 | ["Reality"], 140 | [ 141 | "Reality", 142 | "Reality Drama", 143 | "Courtroom Reality", 144 | "Occupational Reality", 145 | "Celebrity Reality", 146 | ], 147 | ], 148 | [ 149 | ["Documentary"], 150 | [ 151 | "Documentaries", 152 | "Social & Cultural Documentaries", 153 | "Science and Nature Documentaries", 154 | "Miscellaneous Documentaries", 155 | "Crime Documentaries", 156 | "Travel & Adventure Documentaries", 157 | "Sports Documentaries", 158 | "Military Documentaries", 159 | "Political Documentaries", 160 | "Foreign Documentaries", 161 | "Religion & Mythology Documentaries", 162 | "Historical Documentaries", 163 | "Biographical Documentaries", 164 | "Faith & Spirituality Documentaries", 165 | ], 166 | ], 167 | [["Biography"], ["Biographical Documentaries", "Inspirational Biographies"]], 168 | [["Thriller"], ["Sci-Fi Thrillers", "Thrillers", "Crime Thrillers"]], 169 | [["Talk"], ["Talk & Variety", "Talk Show"]], 170 | [["Variety"], ["Sketch Comedies"]], 171 | [["Home Improvement"], ["Art & Design", "DIY & How To", "Home Improvement"]], 172 | [["House/garden"], ["Home & Garden"]], 173 | [["Science"], ["Science and Nature Documentaries"]], 174 | [["Nature"], ["Science and Nature Documentaries", "Animals"]], 175 | [["Cooking"], ["Cooking Instruction", "Food & Wine", "Food Stories"]], 176 | [["Travel"], ["Travel & Adventure Documentaries", "Travel"]], 177 | [["Western"], ["Westerns", "Classic Westerns"]], 178 | [["LGBTQ"], ["Gay & Lesbian", "Gay & Lesbian Dramas", "Gay"]], 179 | [["Game show"], ["Game Show"]], 180 | [["Military"], ["Classic War Stories"]], 181 | [ 182 | ["Comedy"], 183 | [ 184 | "Cult Comedies", 185 | "Spoofs and Satire", 186 | "Slapstick", 187 | "Classic Comedies", 188 | "Stand-Up", 189 | "Sports Comedies", 190 | "African-American Comedies", 191 | "Showbiz Comedies", 192 | "Sketch Comedies", 193 | "Teen Comedies", 194 | "Latino Comedies", 195 | "Family Comedies", 196 | ], 197 | ], 198 | [["Crime"], ["Crime Action", "Crime Drama", "Crime Documentaries"]], 199 | [["Crime drama"], ["Crime Drama"]], 200 | [ 201 | ["Drama"], 202 | [ 203 | "Classic Dramas", 204 | "Family Drama", 205 | "Indie Drama", 206 | "Romantic Drama", 207 | "Crime Drama", 208 | ], 209 | ], 210 | ]; 211 | 212 | versions = ["main"]; 213 | 214 | if (process.argv[2]) { 215 | versions = versions.concat(process.argv[2].split(",")); 216 | } 217 | 218 | const plutoIPTV = { 219 | grabJSON: function (callback) { 220 | callback = callback || function () {}; 221 | 222 | console.log("[INFO] Grabbing EPG..."); 223 | 224 | // check for cache 225 | if (fs.existsSync("cache.json")) { 226 | let stat = fs.statSync("cache.json"); 227 | 228 | let now = new Date() / 1000; 229 | let mtime = new Date(stat.mtime) / 1000; 230 | 231 | // it's under 30 mins old 232 | if (now - mtime <= 1800) { 233 | console.log("[DEBUG] Using cache.json, it's under 30 minutes old."); 234 | 235 | callback(fs.readJSONSync("cache.json")); 236 | return; 237 | } 238 | } 239 | 240 | let startMoment = moment(); 241 | 242 | let timeRanges = []; 243 | for (let i = 0; i < 4; i++) { 244 | let endMoment = moment(startMoment).add(6, "hours"); 245 | timeRanges.push([startMoment, endMoment]); 246 | startMoment = endMoment; 247 | } 248 | 249 | let promises = []; 250 | timeRanges.forEach((timeRange) => { 251 | // 2020-03-24%2021%3A00%3A00.000%2B0000 252 | let startTime = encodeURIComponent( 253 | timeRange[0].format("YYYY-MM-DD HH:00:00.000ZZ") 254 | ); 255 | 256 | // 2020-03-25%2005%3A00%3A00.000%2B0000 257 | let stopTime = encodeURIComponent( 258 | timeRange[1].format("YYYY-MM-DD HH:00:00.000ZZ") 259 | ); 260 | 261 | let url = `http://api.pluto.tv/v2/channels?start=${startTime}&stop=${stopTime}`; 262 | console.log(url); 263 | 264 | promises.push( 265 | new Promise((resolve, reject) => { 266 | request(url, function (err, code, raw) { 267 | if (err) { 268 | reject(err); 269 | } else { 270 | resolve(JSON.parse(raw)); 271 | } 272 | }); 273 | }) 274 | ); 275 | }); 276 | 277 | let channelsList = {}; 278 | Promise.all(promises).then((results) => { 279 | results.forEach((channels) => { 280 | channels.forEach((channel) => { 281 | foundChannel = channelsList[channel._id]; 282 | 283 | if (!foundChannel) { 284 | channelsList[channel._id] = channel; 285 | foundChannel = channel; 286 | } else { 287 | foundChannel.timelines = foundChannel.timelines.concat( 288 | channel.timelines 289 | ); 290 | } 291 | }); 292 | }); 293 | 294 | fullChannels = Object.values(channelsList); 295 | sortedChannels = fullChannels.sort( 296 | ({ number: a }, { number: b }) => a - b 297 | ); 298 | console.log("[DEBUG] Using api.pluto.tv, writing cache.json."); 299 | fs.writeFileSync("cache.json", JSON.stringify(sortedChannels)); 300 | callback(sortedChannels); 301 | return; 302 | }); 303 | }, 304 | }; 305 | 306 | module.exports = plutoIPTV; 307 | 308 | function processChannels(version, list) { 309 | let seenChannels = {}; 310 | let channels = []; 311 | list.forEach((channel) => { 312 | if (seenChannels[channel.number]) { 313 | return; 314 | } 315 | seenChannels[channel.number] = true; 316 | channels.push(channel); 317 | }); 318 | 319 | /////////////////// 320 | // M3U8 Playlist // 321 | /////////////////// 322 | 323 | let m3u8 = "#EXTM3U\n\n"; 324 | channels.forEach((channel) => { 325 | let deviceId = uuid1(); 326 | let sid = uuid4(); 327 | if ( 328 | channel.isStitched && 329 | !channel.slug.match(/^announcement|^privacy-policy/) 330 | ) { 331 | let m3uUrl = new URL(channel.stitched.urls[0].url); 332 | let queryString = url.search; 333 | let params = new URLSearchParams(queryString); 334 | 335 | // set the url params 336 | params.set("advertisingId", ""); 337 | params.set("appName", "web"); 338 | params.set("appVersion", "unknown"); 339 | params.set("appStoreUrl", ""); 340 | params.set("architecture", ""); 341 | params.set("buildVersion", ""); 342 | params.set("clientTime", "0"); 343 | params.set("deviceDNT", "0"); 344 | params.set("deviceId", deviceId); 345 | params.set("deviceMake", "Chrome"); 346 | params.set("deviceModel", "web"); 347 | params.set("deviceType", "web"); 348 | params.set("deviceVersion", "unknown"); 349 | params.set("includeExtendedEvents", "false"); 350 | params.set("sid", sid); 351 | params.set("userId", ""); 352 | params.set("serverSideAds", "true"); 353 | 354 | m3uUrl.search = params.toString(); 355 | m3uUrl = m3uUrl.toString(); 356 | 357 | let slug = conflictingChannels.includes(channel.slug) 358 | ? `pluto-${channel.slug}` 359 | : channel.slug; 360 | let logo = channel.colorLogoPNG.path; 361 | let group = channel.category; 362 | let name = channel.name; 363 | let art = channel.featuredImage.path 364 | .replace("w=1600", "w=1000") 365 | .replace("h=900", "h=562"); 366 | let guideDescription = channel.summary 367 | .replace(/(\r\n|\n|\r)/gm, " ") 368 | .replace('"', "") 369 | .replace("”", ""); 370 | 371 | m3u8 = 372 | m3u8 + 373 | `#EXTINF:0 channel-id="${slug}" channel-number="${channel.number}" tvg-logo="${logo}" tvc-guide-art="${art}" tvc-guide-title="${name}" tvc-guide-description="${guideDescription}" group-title="${group}", ${name} 374 | ${m3uUrl} 375 | 376 | `; 377 | console.log("[INFO] Adding " + channel.name + " channel."); 378 | } else { 379 | console.log("[DEBUG] Skipping 'fake' channel " + channel.name + "."); 380 | } 381 | }); 382 | 383 | /////////////////////////// 384 | // XMLTV Programme Guide // 385 | /////////////////////////// 386 | let tv = []; 387 | 388 | ////////////// 389 | // Channels // 390 | ////////////// 391 | channels.forEach((channel) => { 392 | channel.slug = conflictingChannels.includes(channel.slug) 393 | ? `pluto-${channel.slug}` 394 | : channel.slug; 395 | 396 | if ( 397 | channel.isStitched && 398 | !channel.slug.match(/^announcement|^privacy-policy/) 399 | ) { 400 | tv.push({ 401 | name: "channel", 402 | attrs: { id: channel.slug }, 403 | children: [ 404 | { name: "display-name", text: channel.name }, 405 | { name: "display-name", text: channel.number }, 406 | { name: "desc", text: channel.summary }, 407 | { name: "icon", attrs: { src: channel.colorLogoPNG.path } }, 408 | ], 409 | }); 410 | 411 | ////////////// 412 | // Episodes // 413 | ////////////// 414 | console.log("[INFO] Processing channel " + channel.name); 415 | if (channel.timelines) { 416 | channel.timelines.forEach((programme) => { 417 | console.log("[INFO] Adding instance of " + programme.title); 418 | 419 | let episodeParts = programme.episode.description.match( 420 | /\(([Ss](\d+)[Ee](\d+))\)/ 421 | ); 422 | let episodeNumberString; 423 | if (episodeParts) { 424 | episodeNumberString = episodeParts[1]; 425 | } else if ( 426 | programme.episode.season > 0 && 427 | programme.episode.number > 0 428 | ) { 429 | episodeNumberString = `S${programme.episode.season}E${programme.episode.number}`; 430 | } else if (programme.episode.number > 0) { 431 | episodeNumberString = `${programme.episode.number}`; 432 | } 433 | 434 | let isMovie = programme.episode.series.type == "film"; 435 | let isLive = programme.episode.liveBroadcast === true; 436 | 437 | let channelsGenres = []; 438 | let mogrifiedGenres = [...movieGenres, ...seriesGenres]; 439 | mogrifiedGenres.push(["Children", kidsGenres]); 440 | mogrifiedGenres.push(["News", newsGenres]); 441 | mogrifiedGenres.push(["Sports", sportsGenres]); 442 | mogrifiedGenres.push(["Drama", dramaGenres]); 443 | 444 | mogrifiedGenres.forEach((genrePackage) => { 445 | genreName = genrePackage[0]; 446 | genres = genrePackage[1]; 447 | 448 | if ( 449 | genres.includes(programme.episode.genre) || 450 | genres.includes(programme.episode.subGenre) || 451 | genres.includes(channel.category) 452 | ) { 453 | channelsGenres.push(genreName); 454 | } 455 | }); 456 | 457 | let airingArt; 458 | if (isMovie && null != programme.episode.poster) { 459 | airingArt = programme.episode.poster.path; 460 | } else { 461 | airingArt = programme.episode.series.tile.path 462 | .replace("w=660", "w=900") 463 | .replace("h=660", "h=900"); 464 | } 465 | 466 | airing = { 467 | name: "programme", 468 | attrs: { 469 | start: moment(programme.start).format("YYYYMMDDHHmmss ZZ"), 470 | stop: moment(programme.stop).format("YYYYMMDDHHmmss ZZ"), 471 | channel: channel.slug, 472 | }, 473 | children: [ 474 | { name: "title", attrs: { lang: "en" }, text: programme.title }, 475 | { name: "icon", attrs: { src: airingArt } }, 476 | { 477 | name: "date", 478 | text: moment( 479 | programme.episode.clip 480 | ? programme.episode.clip.originalReleaseDate 481 | : null 482 | ).format("YYYYMMDD"), 483 | }, 484 | { 485 | name: "category", 486 | attrs: { lang: "en" }, 487 | text: isMovie ? "Movie" : "Series", 488 | }, 489 | { 490 | name: "series-id", 491 | attrs: { system: "pluto" }, 492 | text: programme.episode.series._id, 493 | }, 494 | ], 495 | }; 496 | 497 | if ( 498 | programme.episode.description && 499 | programme.episode.description != "No information available" 500 | ) { 501 | airing.children.push({ 502 | name: "desc", 503 | attrs: { lang: "en" }, 504 | text: programme.episode.description, 505 | }); 506 | } 507 | if ( 508 | programme.episode.genre && 509 | programme.episode.genre != "No information available" 510 | ) { 511 | airing.children.push({ 512 | name: "category", 513 | attrs: { lang: "en" }, 514 | text: programme.episode.genre, 515 | }); 516 | } 517 | if ( 518 | programme.episode.subGenre && 519 | programme.episode.subGenre != "No information available" 520 | ) { 521 | airing.children.push({ 522 | name: "category", 523 | attrs: { lang: "en" }, 524 | text: programme.episode.subGenre, 525 | }); 526 | } 527 | if (episodeNumberString && !isMovie && !isLive) { 528 | airing.children.push({ 529 | name: "episode-num", 530 | attrs: { system: "onscreen" }, 531 | text: episodeNumberString, 532 | }); 533 | } 534 | if (!isMovie && !isLive) { 535 | airing.children.push({ 536 | name: "episode-num", 537 | attrs: { system: "pluto" }, 538 | text: programme.episode._id, 539 | }); 540 | } 541 | 542 | let oad = programme.episode.clip 543 | ? programme.episode.clip.originalReleaseDate 544 | : null; 545 | if (isLive) { 546 | airing.children.push({ 547 | name: "live", 548 | }); 549 | airing.children.push({ 550 | name: "episode-num", 551 | attrs: { system: "original-air-date" }, 552 | text: moment(programme.start).format("YYYYMMDDHHmmss ZZ"), 553 | }); 554 | } else if (oad) { 555 | airing.children.push({ 556 | name: "episode-num", 557 | attrs: { system: "original-air-date" }, 558 | text: oad, 559 | }); 560 | } 561 | 562 | let uniqueGenres = channelsGenres.filter(function (item, pos) { 563 | return channelsGenres.indexOf(item) == pos; 564 | }); 565 | 566 | uniqueGenres.forEach((genre) => { 567 | airing.children.push({ 568 | name: "category", 569 | attrs: { lang: "en" }, 570 | text: genre, 571 | }); 572 | }); 573 | 574 | let subTitle = 575 | programme.title == programme.episode.name 576 | ? "" 577 | : programme.episode.name; 578 | if (!isMovie && subTitle) { 579 | airing.children.push({ 580 | name: "sub-title", 581 | attrs: { lang: "en" }, 582 | text: subTitle, 583 | }); 584 | } 585 | 586 | tv.push(airing); 587 | }); 588 | } 589 | } 590 | }); 591 | 592 | let epg = j2x( 593 | { tv }, 594 | { 595 | prettyPrint: true, 596 | escape: true, 597 | } 598 | ); 599 | 600 | epgFileName = version == "main" ? "epg.xml" : `${version}-epg.xml`; 601 | playlistFileName = 602 | version == "main" ? "playlist.m3u" : `${version}-playlist.m3u`; 603 | 604 | fs.writeFileSync(epgFileName, epg); 605 | console.log(`[SUCCESS] Wrote the EPG to ${epgFileName}!`); 606 | 607 | fs.writeFileSync(playlistFileName, m3u8); 608 | console.log(`[SUCCESS] Wrote the M3U8 tuner to ${playlistFileName}!`); 609 | } 610 | 611 | versions.forEach((version) => { 612 | plutoIPTV.grabJSON(function (channels) { 613 | processChannels(version, channels); 614 | }); 615 | }); 616 | --------------------------------------------------------------------------------