├── .env.example ├── .github └── ISSUE_TEMPLATE │ ├── config.yml │ └── submit-addon.yaml ├── .gitignore ├── LICENSE ├── README.md ├── build.js ├── config.js ├── lib ├── cache.js ├── discord.js ├── graphql.js ├── html.js ├── issueToMeta.js └── slug.js ├── netlify.toml ├── out └── folder ├── package-lock.json ├── package.json ├── resources ├── ionicons.5.5.2.js ├── isMobile.js ├── isotope.3.0.6.pkgd.min.js ├── jquery-3.6.1.min.js ├── stremio_community_logo.png └── styles.css ├── template ├── addon │ └── index.html └── home │ ├── index.html │ └── list-addon.html └── trusted_publishers.json /.env.example: -------------------------------------------------------------------------------- 1 | TOKEN="github Token" 2 | DISCORD_WEBHOOK="discord webhook" 3 | REPO="stremio-addons-list" 4 | REPO_AUTHOR="danamag" 5 | DOMAIN="stremio-addons.com" -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/submit-addon.yaml: -------------------------------------------------------------------------------- 1 | name: Publish Stremio Addon 2 | description: Submit a new Stremio Addon to the Unofficial Stremio Addons List 3 | title: "Addon Name" 4 | labels: ["pending approval"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thank you for submitting a new Stremio addon! 10 | - type: input 11 | id: addon-url 12 | attributes: 13 | label: Addon Manifest URL 14 | description: What is the addon manifest URL? (ends with "/manifest.json") 15 | placeholder: ex. https://addon.server.com/manifest.json 16 | validations: 17 | required: true 18 | - type: textarea 19 | id: addon-description 20 | attributes: 21 | label: Addon Description 22 | description: Describe the addon you are submitting. 23 | placeholder: My amazing new addon! 24 | validations: 25 | required: false 26 | - type: dropdown 27 | id: lang 28 | attributes: 29 | label: Language of Content 30 | description: Choose the language in which the streams / metadata show. (if applicable) 31 | options: 32 | - Multilingual (Default) 33 | - Abkhazian (ab-AB) 34 | - Afar (aa-AA) 35 | - Afrikaans (af-ZA) 36 | - Akan (ak-AK) 37 | - Albanian (sq-AL) 38 | - Amharic (am-AM) 39 | - Arabic (ar-SA) 40 | - Arabic (ar-AE) 41 | - Arabic (ar-DZ) 42 | - Aragonese (an-AN) 43 | - Armenian (hy-HY) 44 | - Assamese (as-AS) 45 | - Avaric (av-AV) 46 | - Avestan (ae-AE) 47 | - Aymara (ay-AY) 48 | - Azerbaijani (az-AZ) 49 | - Bambara (bm-BM) 50 | - Bashkir (ba-BA) 51 | - Basque (eu-ES) 52 | - Belarusian (be-BY) 53 | - Bengali (bn-BD) 54 | - Bislama (bi-BI) 55 | - Bosnian (bs-BS) 56 | - Breton (br-BR) 57 | - Bulgarian (bg-BG) 58 | - Burmese (my-MY) 59 | - Cantonese (cn-CN) 60 | - Catalan; Valencian (ca-ES) 61 | - Central Khmer (km-KM) 62 | - Chamorro (ch-GU) 63 | - Chechen (ce-CE) 64 | - Chichewa; Chewa; Nyanja (ny-NY) 65 | - Chinese (zh-CN) 66 | - Chinese (zh-HK) 67 | - Chinese (zh-SG) 68 | - Chinese (zh-TW) 69 | - Chuvash (cv-CV) 70 | - Cornish (kw-KW) 71 | - Corsican (co-CO) 72 | - Cree (cr-CR) 73 | - Croatian (hr-HR) 74 | - Czech (cs-CZ) 75 | - Danish (da-DK) 76 | - Divehi; Dhivehi; Maldivian (dv-DV) 77 | - Dutch; Flemish (nl-NL) 78 | - Dutch; Flemish (nl-BE) 79 | - Dzongkha (dz-DZ) 80 | - English (en-US) 81 | - English (en-AU) 82 | - English (en-CA) 83 | - English (en-GB) 84 | - English (en-IE) 85 | - English (en-NZ) 86 | - Esperanto (eo-EO) 87 | - Estonian (et-EE) 88 | - Ewe (ee-EE) 89 | - Faroese (fo-FO) 90 | - Fijian (fj-FJ) 91 | - Finnish (fi-FI) 92 | - French (fr-FR) 93 | - French (fr-CA) 94 | - Fulah (ff-FF) 95 | - Gaelic; Scottish Gaelic (gd-GB) 96 | - Galician (gl-ES) 97 | - Ganda (lg-LG) 98 | - Georgian (ka-GE) 99 | - German (de-DE) 100 | - German (de-AT) 101 | - German (de-CH) 102 | - Greek (el-GR) 103 | - Guarani (gn-GN) 104 | - Gujarati (gu-GU) 105 | - Haitian; Haitian Creole (ht-HT) 106 | - Hausa (ha-HA) 107 | - Hebrew (he-IL) 108 | - Herero (hz-HZ) 109 | - Hindi (hi-IN) 110 | - Hiri Motu (ho-HO) 111 | - Hungarian (hu-HU) 112 | - Icelandic (is-IS) 113 | - Ido (io-IO) 114 | - Igbo (ig-IG) 115 | - Indian (General) 116 | - Indonesian (id-ID) 117 | - Inuktitut (iu-IU) 118 | - Inupiaq (ik-IK) 119 | - Irish (ga-IE) 120 | - Italian (it-IT) 121 | - Japanese (ja-JP) 122 | - Javanese (jv-JV) 123 | - Kannada (kn-IN) 124 | - Kanuri (kr-KR) 125 | - Kashmiri (ks-KS) 126 | - Kazakh (kk-KZ) 127 | - Kikuyu; Gikuyu (ki-KI) 128 | - Kinyarwanda (rw-RW) 129 | - Kirghiz; Kyrgyz (ky-KG) 130 | - Komi (kv-KV) 131 | - Kongo (kg-KG) 132 | - Korean (ko-KR) 133 | - Kurdish (ku-KU) 134 | - Lao (lo-LO) 135 | - Latin (la-LA) 136 | - Latvian (lv-LV) 137 | - Lingala (ln-LN) 138 | - Lithuanian (lt-LT) 139 | - Luba-Katanga (lu-LU) 140 | - Luxembourgish; Letzeburgesch (lb-LB) 141 | - Macedonian (mk-MK) 142 | - Malagasy (mg-MG) 143 | - Malay (ms-MY) 144 | - Malay (ms-SG) 145 | - Malayalam (ml-IN) 146 | - Maltese (mt-MT) 147 | - Manx (gv-GV) 148 | - Maori (mi-MI) 149 | - Marathi (mr-IN) 150 | - Marshallese (mh-MH) 151 | - Moldavian; Moldovan (mo-MO) 152 | - Mongolian (mn-MN) 153 | - Nauru (na-NA) 154 | - Navajo; Navaho (nv-NV) 155 | - Ndonga (ng-NG) 156 | - Nepali (ne-NE) 157 | - Northern Sami (se-SE) 158 | - Norwegian (no-NO) 159 | - Occitan (post 1500) (oc-OC) 160 | - Ojibwa (oj-OJ) 161 | - Oriya (or-OR) 162 | - Oromo (om-OM) 163 | - Pali (pi-PI) 164 | - Panjabi; Punjabi (pa-IN) 165 | - Persian (fa-IR) 166 | - Polish (pl-PL) 167 | - Portuguese (pt-PT) 168 | - Brazilian Portuguese (pt-BR) 169 | - Pushto; Pashto (ps-PS) 170 | - Quechua (qu-QU) 171 | - Romanian (ro-RO) 172 | - Romansh (rm-RM) 173 | - Rundi (rn-RN) 174 | - Russian (ru-RU) 175 | - Samoan (sm-SM) 176 | - Sango (sg-SG) 177 | - Sanskrit (sa-SA) 178 | - Sardinian (sc-SC) 179 | - Serbian (sr-RS) 180 | - Serbo-Croatian (sh-SH) 181 | - Shona (sn-SN) 182 | - Sichuan Yi; Nuosu (ii-II) 183 | - Sindhi (sd-SD) 184 | - Sinhala; Sinhalese (si-LK) 185 | - Slovak (sk-SK) 186 | - Slovenian (sl-SI) 187 | - Somali (so-SO) 188 | - Sotho, Southern (st-ST) 189 | - Spanish; Castilian (es-ES) 190 | - Spanish; Castilian (es-MX) 191 | - Sundanese (su-SU) 192 | - Swahili (sw-SW) 193 | - Swati (ss-SS) 194 | - Swedish (sv-SE) 195 | - Tagalog (tl-PH) 196 | - Tahitian (ty-TY) 197 | - Tajik (tg-TG) 198 | - Tamil (ta-IN) 199 | - Tatar (tt-TT) 200 | - Telugu (te-IN) 201 | - Thai (th-TH) 202 | - Tibetan (bo-BO) 203 | - Tigrinya (ti-TI) 204 | - Tonga (Tonga Islands) (to-TO) 205 | - Tsonga (ts-TS) 206 | - Tswana (tn-TN) 207 | - Turkish (tr-TR) 208 | - Turkmen (tk-TK) 209 | - Twi (tw-TW) 210 | - Uighur; Uyghur (ug-UG) 211 | - Ukrainian (uk-UA) 212 | - Urdu (ur-UR) 213 | - Uzbek (uz-UZ) 214 | - Venda (ve-VE) 215 | - Vietnamese (vi-VN) 216 | - Volapük (vo-VO) 217 | - Walloon (wa-WA) 218 | - Welsh (cy-GB) 219 | - Western Frisian (fy-FY) 220 | - Wolof (wo-WO) 221 | - Xhosa (xh-XH) 222 | - Yiddish (yi-YI) 223 | - Yoruba (yo-YO) 224 | - Zhuang; Chuang (za-ZA) 225 | - Zulu (zu-ZA) 226 | - type: checkboxes 227 | id: labels 228 | attributes: 229 | label: Choose Labels 230 | description: Choose the labels that best describe your addon. 231 | options: 232 | - label: anime 233 | - label: asian drama 234 | - label: bollywood 235 | - label: cartoons 236 | - label: debrid support 237 | - label: http streams 238 | - label: live tv 239 | - label: metadata 240 | - label: misc 241 | - label: movies 242 | - label: music 243 | - label: nsfw 244 | - label: podcasts 245 | - label: radios 246 | - label: subtitles 247 | - label: torrents 248 | - label: tv shows 249 | - label: video games 250 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Build 38 | out/ 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Microbundle cache 60 | .rpt2_cache/ 61 | .rts2_cache_cjs/ 62 | .rts2_cache_es/ 63 | .rts2_cache_umd/ 64 | 65 | # Optional REPL history 66 | .node_repl_history 67 | 68 | # Output of 'npm pack' 69 | *.tgz 70 | 71 | # Yarn Integrity file 72 | .yarn-integrity 73 | 74 | # dotenv environment variables file 75 | .env 76 | .env.test 77 | 78 | # parcel-bundler cache (https://parceljs.org/) 79 | .cache 80 | 81 | # Next.js build output 82 | .next 83 | 84 | # Nuxt.js build / generate output 85 | .nuxt 86 | dist 87 | 88 | # Gatsby files 89 | .cache/ 90 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 91 | # https://nextjs.org/blog/next-9-1#public-directory-support 92 | # public 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # TernJS port file 107 | .tern-port 108 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Stripes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Great List of Stremio Addons 2 | > [!WARNING] 3 | > This repository is no longer maintained. Try out the new Stremio Community Addon list here (BETA): [beta.stremio-addons.net](https://beta.stremio-addons.net/). 4 | 5 | > [!NOTE] 6 | > Due to recent abuse of this repository (malicious addons, spam) by a user called [vancengvn](https://github.com/Vance-ng-vn) we were forced to implement a moderation system. New addon submissions now need explicit approval from a contributor to be published - unless you are on the [trusted publishers](./trusted_publishers.json) list. Please contact a moderator on Discord or Reddit if your addon hasn't been approved after 7 days. 7 | 8 | To see the great list of Stremio Addons go to [the website](https://stremio-addons.com/). 9 | 10 | To submit a new addon to the list, use [this link](https://github.com/danamag/stremio-addons-list/issues/new?assignees=&labels=pending+approval&template=submit-addon.yaml&title=Addon+Name). 11 | 12 | To upvote / downvote an addon, find it in [the issues](https://github.com/danamag/stremio-addons-list/issues) and react with a thumbs up / down to the issue comment. 13 | 14 | To comment on an addon, find it in [the issues](https://github.com/danamag/stremio-addons-list/issues) and comment on the issue, this will update the comments on the site too. (you can also comment with GitHub on the website directly) 15 | 16 | To get notifications about new addons press the "Watch" button at the top right of this page. (or [join the Discord](https://discord.gg/zNRf6YF), all new addons are announced on the #bots channel) 17 | 18 | 19 | ## How Can I Help? 20 | 21 | This project is completely automated, what addons get in the list and what addons are removed is decided by each and every one of you, the only requirement is a free GitHub profile. 22 | 23 | So here's how you can help: 24 | - add addons that are working (and not yet) in the list (by creating a new GitHub issue) 25 | - give a thumbs up / down to the addons that are already in the list (through GitHub comment reactions) 26 | - comment and discuss addons (through the GitHub commenting system) 27 | 28 | 29 | ## Project Features 30 | 31 | - anyone can publish an addon 32 | - publishers can choose labels 33 | - publishers can choose content language (if applicable) 34 | - everyone can vote on addons 35 | - all addons are ordered by community votes 36 | - addon labels 37 | - filter by addon labels 38 | - comments for addon pages 39 | - rich text comments 40 | - comments support reactions (emojis) 41 | - "is it online?" real-time check for addons (on the addon page) 42 | - notifications for new addon releases (through GitHub followship) 43 | - search addons 44 | - show comment count for each addon in the list 45 | - Discord chat notifications for new addon releases 46 | 47 | ## Addon Submission Rules 48 | 49 | We do not tolerate any abuse or misuse of user-submitted addons. The following behaviors are strictly prohibited: 50 | 51 | 1. **Prohibited Content** 52 | Users must not submit: 53 | - Abusive, harassing, discriminatory, or threatening content. 54 | - Defamatory, false, or harmful material. 55 | - Malicious code, ads, viruses, or any form of malware. 56 | - Content promoting illegal activities. 57 | 58 | 2. **Prohibited Actions** 59 | - Impersonating others or submitting misleading information. 60 | - Engaging in spamming, phishing, or fraud. 61 | - Attempting to bypass security or moderation measures. 62 | 63 | 3. **Consequences** 64 | Violation of the terms above will result in your addon being removed and unpublished from this repository. Multiple violations may result in a permanent ban. Our moderation team reserves the right to remove you from other community-moderated platforms such as (but not limited to) our Reddit and Discord communities. 65 | 66 | 4. **Reporting** 67 | Please contact a moderator to report any abusive or harmful content, including suspected malware. We will take appropriate action. 68 | 69 | *By submitting an addon to this repository, you automatically agree to the terms above.* 70 | 71 | ## How it works 72 | 73 | When submitting an addon to the list, a github issue is created to represent this submission. If the original poster closes their issue, or someone with access to the project closes the issue, the addon will be removed from the list. If the project detects an invalid submission it will automatically close the issue and set an explanatory label for the reason. 74 | 75 | All addons in the list are ordered by the thumbs up / down votes of the github issues, if an addon has less than -10 votes it is removed from the list. 76 | 77 | If an addon manifest has been unreachable for more than 10 days, it will be removed from the list. 78 | 79 | Labels for addons are a 1:1 copy of github labels used for issues, the colors chosen for these labels on github will also be used on the site. 80 | 81 | Commenting on an issue will also add the comments to the dedicated addon page on the website. 82 | 83 | The site is currently refreshed based on the following triggers: 84 | - a new issue is created (a new addon was submitted) 85 | - a new release was created 86 | - a new commit was made 87 | - a label was created, edited or removed 88 | - a new comment was made to an issue (to update comment count) 89 | - daily at 08:15 by GitHub Actions (to update votes if no other event did) 90 | 91 | 92 | ## Fork me 93 | 94 | This project is available under the MIT license and uses exclusively free resources. (GitHub WebHooks and Netlify) 95 | 96 | To create your own Stremio Addons list: 97 | - fork this project 98 | - enable issues for your fork: `Settings` > `Features` > `Issues` 99 | - edit `/config.js` with your repo information 100 | - connect Netlify to your GitHub fork (on `main` branch) 101 | - in Netlify: `Sites` > `(choose site)` > `Site Settings` > `Build & deploy` > `Build settings`: Base directory = "Not set" ; Build command = "npm run build" ; Publish directory = "out/" 102 | - create a GitHub API token: `Settings` > `Developer Settings` (bottom left) > `Personal access tokens` (left side) > `Tokens` (classic) > `Generate new token` (copy the token to clipboard) 103 | - add GitHub API token to Netlify: `Sites` > `(choose site)` > `Site Settings` > `Build & deploy` > `Environment` > `Environment Variables` > (add key called "TOKEN" and paste GitHUB API token) 104 | - create a Netlify Hook: `Sites` > `(choose site)` > `Site Settings` > `Build & deploy` > `Continuous deployment` > `Build hooks` > `Add build hook` > (copy the URL from the hook) 105 | - create a GitHub WebHook: `Settings` > `WebHooks` (left side menu) > `Add WebHook` (top right button): Payload URL = URL copied from Netlify ; choose "Let me select individual events" ; ensure "Active" is enabled 106 | - choose events that will trigger the website builds: Issues; Labels; Releases; Pushes (optional events: Issue comments) 107 | - press "Add webhook" 108 | - add GitHub Repository Secret for Netlify Hook: `Settings` > `Secrets` (left side menu) > `Actions` > `New repository secret`: Name = "NETLIFY_BUILD_WEBHOOK" ; Paste URL copied from Netlify as Secret (this is needed for the GitHub Action from `/.github/workflows/main.yml` which will do a daily build to update votes) 109 | - on Github go to: `Issues` > `Labels` (top right button) > (add labels that you need, delete labels that you don't need) (if you want to use the default labels, check the `/.github/ISSUE_TEMPLATE/submit-addon.yaml` file to see the list) 110 | - open `/.github/ISSUE_TEMPLATE/submit-addon.yaml` and edit the labels to match the ones you use for your addons list (if not using the default labels) 111 | 112 | You're done! 113 | 114 | 115 | ### Extras 116 | 117 | - you can also set the "DISCORD_WEBHOOK" environment variable in Netlify, this will make it notify on Discord every time a new addon is published (a new issue is created) 118 | - by default this project only allows submitting issues with the "Publish Stremio Addon" issue template, if you want to allow blank issues too, then edit the `/.github/ISSUE_TEMPLATE/config.yml` file and set `blank_issues_enabled: true` 119 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const slug = require('./lib/slug') 3 | const needle = require('needle') 4 | const asyncQueue = require('async.queue') 5 | const config = require('./config') 6 | const graphql = require('./lib/graphql') 7 | const getCached = require('./lib/cache') 8 | const processHtml = require('./lib/html') 9 | const issueToMeta = require('./lib/issueToMeta') 10 | const sendDiscordMessage = require('./lib/discord') 11 | const trustedPublishers = require('./trusted_publishers.json') 12 | 13 | getCached().then(cached => { 14 | if (cached.time && cached.time > Date.now() - config['prefer-cached-for']) { 15 | console.log('cache will be preferred over refreshing manifest data') 16 | cached.prefer = true 17 | } 18 | graphql.getAllPosts().then(data => { 19 | const addons = [] 20 | const addons_collection = [] 21 | const all_labels = [{ color: 'A08C80', name: 'show all' }] 22 | const noDups = [] 23 | // issues are ordered chronologically 24 | data.forEach(addon => { 25 | const meta = issueToMeta(addon) 26 | 27 | if (meta && meta.name && meta.url) { 28 | // skip addons that are not approved (yet) by our moderators 29 | if (!(meta.labels ?? []).some(label => label.id === config['label-id-approved'])) { 30 | const publisher = (addon.author || {}).login 31 | 32 | // automatically approve addons from trusted publishers 33 | if (publisher && trustedPublishers.includes(publisher)) { 34 | console.log(`approving addon '${meta.name}' from trusted publisher '${publisher}'`) 35 | graphql.syncLabelsQueue.push({ postId: meta.postId, proposedLabels: ['approved'], allLabels: [{ name: 'approved', id: config['label-id-approved'] }] }) 36 | } else { 37 | console.log(`skipping unapproved addon '${meta.name}'`) 38 | } 39 | 40 | return 41 | } 42 | 43 | if (meta.score > config['minimum-score']) { 44 | if (noDups.includes(meta.url)) { 45 | console.log('closing issue due to duplication: ' + meta.name) 46 | graphql.closeIssueQueue.push({ postId: meta.postId, label: config['label-id-for-duplicate'] }) 47 | return 48 | } 49 | noDups.push(meta.url) 50 | meta.labels.forEach(label => { 51 | if (label.name && !all_labels.some(el => label.name === el.name)) 52 | all_labels.push(label) 53 | }) 54 | addons.push(meta) 55 | } else { 56 | console.log('closing issue due to low score: ' + meta.name) 57 | graphql.closeIssueQueue.push({ postId: meta.postId, label: config['label-id-for-low-score'] }) 58 | } 59 | } else { 60 | console.log('closing issue due to submission being invalid: ' + meta.name) 61 | graphql.closeIssueQueue.push({ postId: meta.postId, label: config['label-id-for-invalid'] }) 62 | } 63 | }) 64 | 65 | // ensure that labels are the same as proposed by user submitting 66 | addons.forEach(addon => { 67 | if (addon.postId && addon.proposedLabels.length) { 68 | const addonLabelNames = addon.labels.map(label => label.name) 69 | 70 | // we only sync labels on approved issues 71 | if (!addonLabelNames.includes('approved')) { 72 | console.log(`skipping label sync for unapproved addon '${addon.name}'`) 73 | return 74 | } 75 | 76 | const diff = addon.proposedLabels.filter(x => !addonLabelNames.includes(x)) 77 | 78 | if (diff.length) { 79 | // proposed labels are different than issue labels 80 | console.log('syncing labels for: ' + addon.name) 81 | console.log(addon.proposedLabels) 82 | graphql.syncLabelsQueue.push({ postId: addon.postId, proposedLabels: ['approved', ...addon.proposedLabels], allLabels: all_labels }) 83 | } 84 | 85 | } 86 | }) 87 | 88 | const dir = config['build-dir'] 89 | 90 | if (!fs.existsSync(dir)) fs.mkdirSync(dir) 91 | 92 | const listHtml = [] 93 | const newAddons = [] 94 | 95 | const queue = asyncQueue((task, cb) => { 96 | const processManifest = (addonManifest, meta, rip) => { 97 | if (!addonManifest) { 98 | console.log('warning: could not find addon manifest for: ' + task.name) 99 | graphql.closeIssueQueue.push({ postId: meta.postId, label: rip ? config['label-id-for-inactive'] : config['label-id-for-unreachable'] }) 100 | cb() 101 | return 102 | } 103 | if (addonManifest.name && addonManifest.name !== meta.name) { 104 | console.log('warning: github issue name is different than addon name: ' + task.name) 105 | graphql.updateTitleQueue.push({ postId: meta.postId, title: addonManifest.name }) 106 | } 107 | addons_collection.push({ 108 | transportUrl: task.url, 109 | transportName: 'http', 110 | manifest: addonManifest, 111 | }) 112 | if (cached.catalog.length && !cached.catalog.find(el => ((el || {}).manifest || {}).id === addonManifest.id)) { 113 | task.manifest = addonManifest 114 | if (!config.blockedManifests.includes(task.url)) 115 | newAddons.push(task) 116 | } 117 | 118 | task.labels.pop('approved') // we shouldn't show the "approved" label on the addon page 119 | let labelsHtml = task.labels.map(el => el.name.split(' ').join('-')).join(' ') 120 | if (labelsHtml) labelsHtml = ' ' + labelsHtml 121 | 122 | const lowerCaseName = addonManifest.name.toLowerCase() 123 | const keywordsForAddonPage = config['addon-keywords'].split('{}').join(lowerCaseName) 124 | 125 | const installButton = !(addonManifest.behaviorHints || {}).configurationRequire ? 'Install Install (Web) Copy Link' : ''; 126 | const configButton = (addonManifest.behaviorHints || {}).configurable ? 'Configure' : '' 127 | const commentsButton = task.commentCount ? ` ${task.commentCount}` : '' 128 | const language = task.language && task.language !== 'Multilingual' && task.language !== 'None' ? `
${task.language} Content
` : '' 129 | const addonsScoreFaded = !task.ups && !task.downs ? ' addon-score-faded' : '' 130 | 131 | const labelsForHomeHeader = task.labels.map(el => `${el.name}`).join('') 132 | const labelsForHomeAddon = task.labels.map(el => `${el.name}`).join('') 133 | 134 | const map = { 135 | '{home-netlify-domain}': config['netlify-domain'], 136 | '{addon-page-title-append}': config['meta-addon-title-append'], 137 | '{labels}': labelsHtml, 138 | '{addon-id}': addonManifest.id, 139 | '{addon-version}': addonManifest.version, 140 | '{addon-title}': addonManifest.name, 141 | '{addon-description}': addonManifest.description || '', 142 | '{addon-keywords}': keywordsForAddonPage, 143 | '{addon-logo}': addonManifest.logo || addonManifest.icon, 144 | '{addon-types}': labelsForHomeHeader, 145 | '{addon-types-small}': labelsForHomeAddon, 146 | '{addon-score}': task.score, 147 | '{addon-ups}': task.ups, 148 | '{addon-downs}': task.downs, 149 | '{addons-score-faded}': addonsScoreFaded, 150 | '{install-button}': installButton, 151 | '{configure-button}': configButton, 152 | '{comments-button}': commentsButton, 153 | '{addon-page}': `${slug(addonManifest.name)}.html`, 154 | '{issue-url}': task.issueUrl, 155 | '{issue-number}': task.issueNumber, 156 | '{repo-name}': config.author+'/'+config.repository, 157 | '{addon-language}': language, 158 | '{addon-url}': task.url, 159 | } 160 | 161 | const addonHtml = processHtml('homePageAddon', map) 162 | 163 | task.labels = [{ color: 'A08C80', name: ' all addons' }].concat(task.labels) 164 | const labelsForAddonPage = task.labels.map(el => `<${'a href="https://' + config['netlify-domain'] + '/' + (el.name === ' all addons' ? '' : '?label=' + el.name.split(' ').join('-')) + '"'} class="label label-addon-page" style="background-color: #${el.color}">${el.name}`).join('') 165 | map['{addon-types-links}'] = labelsForAddonPage 166 | 167 | const parsedAddonPage = processHtml('addonPage', map) 168 | 169 | console.log('creating page for addon: ' + addonManifest.name) 170 | fs.writeFileSync(`${dir}/${slug(addonManifest.name)}.html`, parsedAddonPage) 171 | task.labels.shift() // remove "all addons" prefix from labels 172 | listHtml.push(addonHtml) 173 | cb() 174 | } 175 | const findCachedManifest = () => { 176 | let cachedManifest 177 | cached.catalog.some(oldAddon => { 178 | if (oldAddon.transportUrl === task.url) { 179 | if (!cached.prefer) 180 | console.log('warning: using cached manifest for: ' + task.name) 181 | cachedManifest = oldAddon.manifest 182 | return true 183 | } 184 | }) 185 | return cachedManifest 186 | } 187 | if (cached.prefer) { 188 | const cachedManifest = findCachedManifest() 189 | if (cachedManifest) { 190 | processManifest(cachedManifest, task) 191 | return 192 | } 193 | } 194 | needle.get(task.url, config.needle, (err, resp, body) => { 195 | let addonManifest 196 | let rip 197 | if ((body || {}).id && body.version) { 198 | addonManifest = body 199 | cached.lastReached[task.url] = Date.now() 200 | } else if (cached.catalog.length) { 201 | if (cached.lastReached[task.url] && Date.now() - cached.lastReached[task.url] > config['maximum-unreachable']) { 202 | rip = true 203 | } else { 204 | addonManifest = findCachedManifest() 205 | } 206 | } 207 | processManifest(addonManifest, task, rip) 208 | }) 209 | }, 1) 210 | 211 | queue.drain = () => { 212 | if (config.DISCORD_WEBHOOK && newAddons.length) 213 | sendDiscordMessage(newAddons.filter(addon => !config.blockedManifests.includes(addon.url))) 214 | console.log('copying resources (styles, js, images)') 215 | fs.readdirSync('./resources').forEach(file => { 216 | const filePath = `./resources/${file}` 217 | if (fs.existsSync(filePath) && !fs.lstatSync(filePath).isDirectory()) { 218 | console.log(`copied ${file} resource`) 219 | fs.copyFileSync(filePath, `${dir}/${file}`) 220 | } 221 | }) 222 | fs.copyFileSync('./resources/styles.css', `${dir}/styles.css`) 223 | 224 | const manifest = { 225 | id: 'community.stremio.stremio-addons-list', 226 | version: '1.0.0', 227 | name: 'Stremio Community Addons List', 228 | description: 'Stremio Community Addons List', 229 | types: ["movie", "series", "channel", "tv"], 230 | resources: ['addon_catalog'], 231 | catalogs: [], 232 | addonCatalogs: [{ 233 | type: 'all', 234 | id: 'community', 235 | name: 'Community (External)', 236 | }], 237 | }; 238 | 239 | console.log('creating manifest file') 240 | fs.writeFileSync(`${dir}/manifest.json`, JSON.stringify(manifest)) 241 | 242 | console.log('creating addon_catalog file') 243 | fs.mkdirSync(`${dir}/addon_catalog/all`, { recursive: true }); 244 | fs.writeFileSync(`${dir}/addon_catalog/all/community.json`, JSON.stringify({ 245 | addons: addons_collection 246 | })) 247 | 248 | console.log('creating addons catalog json file') 249 | fs.writeFileSync(`${dir}/catalog.json`, JSON.stringify(addons_collection)) 250 | 251 | console.log('creating last reached json file') 252 | fs.writeFileSync(`${dir}/lastReached.json`, JSON.stringify(cached.lastReached)) 253 | 254 | console.log('creating home page') 255 | // move "misc" label to end of list 256 | const miscLabelIndex = all_labels.findIndex(label => label.name === 'misc') 257 | if (miscLabelIndex > -1) 258 | all_labels.push(all_labels.splice(miscLabelIndex, 1)[0]) 259 | const map = { 260 | '{home-keywords}': config['meta-keywords'], 261 | '{home-page-title}': config['page-title'], 262 | '{home-meta-title}': config['meta-title'], 263 | '{home-netlify-domain}': config['netlify-domain'], 264 | '{home-favicon}': config['meta-favicon'], 265 | '{home-description}': config['meta-description'], 266 | '{repo-name}': config.author+'/'+config.repository, 267 | '{labels-list}': all_labels.filter(label => label.name !== "approved").map((el, ij) => `${el.name}`).join(''), 268 | '{addons-list}': listHtml.join(''), 269 | } 270 | const homePage = processHtml('homePage', map) 271 | fs.writeFileSync(`${dir}/index.html`, homePage) 272 | if (!cached.prefer) { 273 | console.log('saving timestamp of last update to json') 274 | fs.writeFileSync(`${dir}/lastUpdate.json`, JSON.stringify({ time: Date.now() })) 275 | } else { 276 | console.log('persisting last known update time because cache was preferred') 277 | fs.writeFileSync(`${dir}/lastUpdate.json`, JSON.stringify({ time: cached.time })) 278 | } 279 | } 280 | 281 | addons.sort((a,b) => { return a.score > b.score ? -1 : 1 }) 282 | 283 | addons.forEach(addon => queue.push(addon)) 284 | 285 | }).catch(e => console.error(e)) 286 | }).catch(e => console.error(e)) 287 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | 3 | module.exports = { 4 | // env vars 5 | "DISCORD_WEBHOOK": process.env.DISCORD_WEBHOOK, 6 | "GITHUB_TOKEN": process.env.TOKEN, 7 | // for repo: danamag/stremio-addons-list 8 | "repository": process.env.REPO || "stremio-addons-list", 9 | "author": process.env.REPO_AUTHOR || "danamag", 10 | "netlify-domain": process.env.DOMAIN || "stremio-addons.com", 11 | "page-title": "Stremio Community Addons List", 12 | // images are located in ./resources/ 13 | "meta-favicon": "stremio_community_logo.png", 14 | "meta-title": "Stremio Addons - Community List", 15 | "meta-description": "Stremio community currated addons list, find the best Stremio addons here.", 16 | "meta-keywords": "stremio addons, no streams, addons, torrentio, piratebay, addons list, what addons", 17 | // for the addon page, {} is replaced with the addon name 18 | "addon-keywords": "{}, {} down, {} down or just me, {} site down, {} not working, {} not found, stremio addons, addons list", 19 | // this gets appended to the title, ex: "TMDB Addon - Stremio Addons" 20 | "meta-addon-title-append": "Stremio Addons", 21 | // for 12h since last full update, it will prefer cached manifests over retrieving new ones 22 | "prefer-cached-for": 12 * 60 * 60 * 1000, 23 | // when min score is reached the addon will be removed / issue closed 24 | "minimum-score": -10, 25 | // when an addon is unreachable for 10 days, remove the submission 26 | "maximum-unreachable": 10 * 24 * 60 * 60 * 1000, 27 | // optional label id, if available it will add a label when closing an issue due to various reasons 28 | // you can only get the label id through the github graphql api 29 | "label-id-for-low-score": "LA_kwDOFVUyTM8AAAABGbO_Bw", // "very low score" 30 | "label-id-for-duplicate": "LA_kwDOFVUyTM8AAAABP8VfgA", // "duplicate" 31 | "label-id-for-invalid": "LA_kwDOFVUyTM8AAAABP8VmVQ", // "invalid addon url" 32 | "label-id-for-inactive": "LA_kwDOFVUyTM8AAAABP8VsIw", // "addon inactive" 33 | "label-id-for-unreachable": "LA_kwDOFVUyTM8AAAABP8Zl7g", // "addon manifest unreachable" 34 | "label-id-approved": "LA_kwDOFVUyTM8AAAABwTaXuQ", // "approved" 35 | "label-id-pending-approval": "LA_kwDOFVUyTM8AAAABwUPwvQ", // "pending approval" 36 | // sane timeouts for needle so it doesn't get stuck in a request 37 | "needle": { "open_timeout": 5000, "response_timeout": 5000, "read_timeout": 5000, "follow_max": 5 }, 38 | // output folder for build 39 | "build-dir": "./out", 40 | "blockedManifests": [ 41 | "https://comet.elfhosted.com/manifest.json", 42 | "https://comet.stremiofr.com/manifest.json", 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /lib/cache.js: -------------------------------------------------------------------------------- 1 | // we get the old files from the deployment 2 | const needle = require('needle') 3 | const config = require('../config') 4 | 5 | function isObject(obj) { 6 | return typeof obj === 'object' && 7 | !Array.isArray(obj) && 8 | obj !== null 9 | } 10 | 11 | const getCached = () => { 12 | return new Promise((resolve, reject) => { 13 | const cached = { catalog: [] } 14 | needle.get(`https://${config['netlify-domain']}/lastUpdate.json`, config.needle, (err, resp, body) => { 15 | // if an update was done less than 12h ago, then prefer updating from cache 16 | if ((body || {}).time) { 17 | cached.time = body.time 18 | console.log('got last known website update time') 19 | } else { 20 | console.log('warning: could not get last update time') 21 | } 22 | 23 | needle.get(`https://${config['netlify-domain']}/catalog.json`, config.needle, (err, resp, body) => { 24 | if ((body || []).length && body[0].transportUrl && body[0].transportName && body[0].manifest) { 25 | console.log('loaded old addon catalog') 26 | cached.catalog = body 27 | } else { 28 | console.log('warning: could not load old addon catalog') 29 | } 30 | 31 | needle.get(`https://${config['netlify-domain']}/lastReached.json`, config.needle, (err, resp, body) => { 32 | if (body && isObject(body) && Object.keys(body).length) { 33 | console.log('loaded last reached data') 34 | cached.lastReached = body 35 | } else { 36 | cached.lastReached = {} 37 | console.log('warning: could not load last reached data') 38 | } 39 | resolve(cached) 40 | }) 41 | }) 42 | }) 43 | }) 44 | } 45 | 46 | module.exports = getCached 47 | -------------------------------------------------------------------------------- /lib/discord.js: -------------------------------------------------------------------------------- 1 | const config = require('../config') 2 | const slug = require('../lib/slug') 3 | const needle = require('needle') 4 | 5 | const discordGreeting = () => { 6 | const greetings = ["Hey", "Sup", "Yo", "Knock knock", "Attention", "Yesss", "Oh yeah", "Awesome", "Finally", "Arr", "Ahoy, matey", "Put that cookie down"] 7 | return greetings[Math.floor(Math.random()*greetings.length)] 8 | } 9 | 10 | const sendDiscordMessage = (addons) => { 11 | const payload = { 12 | content: `${discordGreeting()}, ${addons.length > 1 ? ' new addons' : 'a new addon'} appeared in the catalog!`, 13 | embeds: [] 14 | } 15 | addons.forEach(addon => { 16 | const manifest = addon.manifest 17 | const addonUrl = `https://${config['netlify-domain']}/${slug(addon.name)}.html` 18 | const embed = { 19 | title: manifest.name, 20 | url: addonUrl, 21 | thumbnail: { 22 | url: manifest.logo, 23 | }, 24 | description: manifest.description, 25 | color: 9001641, 26 | provider: { 27 | name: 'Install', 28 | url: addonUrl, 29 | } 30 | } 31 | const labels = addon.proposedLabels.length ? addon.proposedLabels : (addon.labels || []).map(label => label.name).filter(el => !! el) 32 | if ((labels || []).length) 33 | embed.fields = [ 34 | { 35 | name: 'Type', 36 | value: labels.map(label => label.toLowerCase()).join(', '), 37 | }, 38 | ] 39 | payload.embeds.push(embed) 40 | }) 41 | const needleOpts = Object.assign({}, config.needle) 42 | needleOpts.json = true 43 | console.log(`sending discord message about ${addons.length} new addons`) 44 | needle.post(config.DISCORD_WEBHOOK, payload, needleOpts, (err, resp, body) => { 45 | if (!err) 46 | console.log(`sent discord message about ${addons.length} new addons`) 47 | else { 48 | console.log(`error when sending discord message about ${addons.length} new addons`) 49 | console.error(err) 50 | } 51 | }) 52 | } 53 | 54 | module.exports = sendDiscordMessage 55 | -------------------------------------------------------------------------------- /lib/graphql.js: -------------------------------------------------------------------------------- 1 | const { graphql } = require('@octokit/graphql') 2 | const asyncQueue = require('async.queue') 3 | const config = require('../config') 4 | 5 | const request = graphql.defaults({ 6 | headers: { 7 | authorization: `token ${config.GITHUB_TOKEN}`, 8 | }, 9 | }) 10 | 11 | const getPosts = (after) => 12 | request( 13 | `{ 14 | repository(name: "${config.repository}", owner: "${config.author}") { 15 | issues(states: [OPEN], first: 100${after ? ', after: "' + after + '"' : ''}) { 16 | pageInfo { 17 | hasNextPage 18 | endCursor 19 | } 20 | nodes { 21 | id 22 | title 23 | number 24 | createdAt 25 | url 26 | body 27 | labels(first: 20) { 28 | nodes { 29 | color 30 | name 31 | id 32 | } 33 | } 34 | comments { 35 | totalCount 36 | } 37 | reactionGroups { 38 | content 39 | users { 40 | totalCount 41 | } 42 | } 43 | author { 44 | login 45 | } 46 | } 47 | } 48 | } 49 | } 50 | ` 51 | ).then((data) => data.repository.issues) 52 | 53 | 54 | const getAllPosts = () => { 55 | return new Promise((resolve, reject) => { 56 | const allItems = [] 57 | const loopPages = after => { 58 | getPosts(after).then(data => { 59 | data = data || {} 60 | data.nodes = data.nodes || [] 61 | data.nodes.forEach(node => allItems.push(node)) 62 | if ((data.pageInfo || {}).hasNextPage && data.pageInfo.endCursor) 63 | loopPages(data.pageInfo.endCursor) 64 | else 65 | resolve(allItems) 66 | }) 67 | } 68 | loopPages() 69 | }) 70 | } 71 | 72 | const syncLabels = (postId, proposedLabels, allLabels) => { 73 | const labels = proposedLabels.map(el => allLabels.find(elm => elm.name === el)).filter(el => !!el).map(el => el.id) 74 | if (!labels.length) return Promise.reject(Error('error: could not find any label id in order to update issue labels')); 75 | return request( 76 | `mutation { 77 | updateIssue(input: {id : "${postId}" , labelIds: ${JSON.stringify(labels)} }){ 78 | issue { 79 | id 80 | title 81 | } 82 | } 83 | } 84 | ` 85 | ) 86 | } 87 | 88 | const updateTitle = (postId, title) => { 89 | return request( 90 | `mutation { 91 | updateIssue(input: {id : "${postId}" , title: "${title}" }){ 92 | issue { 93 | id 94 | title 95 | } 96 | } 97 | } 98 | ` 99 | ) 100 | } 101 | 102 | const closeIssueQueue = asyncQueue((task, cb) => { 103 | closeIssue(task.postId, task.label).then(() => { cb() }).catch(() => { cb() }) 104 | }) 105 | 106 | const closeIssue = (postId, label) => { 107 | // also adds label to the issue if label id provided 108 | return request( 109 | `mutation { 110 | updateIssue(input: {id : "${postId}" , state: CLOSED${label ? ', labelIds: ["' + label + '"]' : ''} }){ 111 | issue { 112 | id 113 | title 114 | } 115 | } 116 | } 117 | `) 118 | } 119 | 120 | const syncLabelsQueue = asyncQueue((task, cb) => { 121 | syncLabels(task.postId, task.proposedLabels, task.allLabels).then(() => { cb() }).catch(() => { cb() }) 122 | }) 123 | 124 | const updateTitleQueue = asyncQueue((task, cb) => { 125 | updateTitle(task.postId, task.title).then(() => { cb() }).catch(() => { cb() }) 126 | }) 127 | 128 | module.exports = { 129 | getPosts, getAllPosts, syncLabels, syncLabelsQueue, updateTitleQueue, closeIssue, closeIssueQueue, 130 | } 131 | -------------------------------------------------------------------------------- /lib/html.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const templateFolder = path.join(__dirname, '..', 'template') 5 | 6 | const addonPageTemplate = fs.readFileSync(path.join(templateFolder, 'addon', 'index.html')).toString() 7 | const homePageTemplate = fs.readFileSync(path.join(templateFolder, 'home', 'index.html')).toString() 8 | const homePageAddonTemplate = fs.readFileSync(path.join(templateFolder, 'home', 'list-addon.html')).toString() 9 | 10 | const processHtml = (templateName, map) => { 11 | let template 12 | if (templateName === 'addonPage') 13 | template = addonPageTemplate 14 | else if (templateName === 'homePage') 15 | template = homePageTemplate 16 | else if (templateName === 'homePageAddon') 17 | template = homePageAddonTemplate 18 | 19 | const re = new RegExp(Object.keys(map).join('|'),'gi') 20 | return template.replace(re, matched => map[matched]) 21 | } 22 | 23 | module.exports = processHtml 24 | -------------------------------------------------------------------------------- /lib/issueToMeta.js: -------------------------------------------------------------------------------- 1 | const config = require('../config') 2 | 3 | const issueToMeta = issue => { 4 | const meta = { 5 | name: issue.title, 6 | url: '', 7 | description: '', 8 | ups: 0, 9 | downs: 0, 10 | commentCount: 0, 11 | issueUrl: issue.url, 12 | proposedLabels: [], 13 | language: 'Multilingual', 14 | } 15 | const chunks = (issue.body || '').split(/\r?\n/) 16 | let readingFor = false 17 | chunks.forEach(chunk => { 18 | if (chunk === '### Addon Manifest URL') 19 | readingFor = 'url' 20 | else if (chunk === '### Addon Description') 21 | readingFor = 'description' 22 | else if (chunk === '### Language of Content') 23 | readingFor = 'language' 24 | else if (chunk === '### Choose Labels') 25 | readingFor = 'labels' 26 | else if (readingFor && chunk) { 27 | if (readingFor === 'url' && meta.url.endsWith('/manifest.json')) return; 28 | if (readingFor === 'labels' && chunk.toLowerCase().startsWith('- [x] ')) { 29 | meta.proposedLabels.push(chunk.replace('- [X] ','').replace('- [x] ', '').trim()) 30 | return 31 | } 32 | if (readingFor === 'language') { 33 | lang = chunk.split('; ')[0].split(' (')[0].trim() 34 | if (lang !== '_No response_') 35 | meta[readingFor] = lang 36 | return 37 | } 38 | meta[readingFor] += chunk 39 | meta[readingFor] = meta[readingFor].trim() 40 | } 41 | }) 42 | if (!meta.url.startsWith('https://') || !meta.url.endsWith('/manifest.json')) 43 | meta.url = '' 44 | if (meta.description === '_No response_') 45 | meta.description = '' 46 | if (meta.name && meta.url) { 47 | const reactionGroups = issue.reactionGroups || [] 48 | meta.labels = (issue.labels || {}).nodes || [] 49 | let score = 0 50 | reactionGroups.forEach(group => { 51 | if ((group.users || {}).totalCount) { 52 | if (group.content === 'THUMBS_UP') { 53 | meta.ups = group.users.totalCount 54 | score += group.users.totalCount 55 | } else if (group.content === 'THUMBS_DOWN') { 56 | meta.downs = group.users.totalCount 57 | score -= group.users.totalCount 58 | } 59 | } 60 | }) 61 | meta.issueNumber = issue.number 62 | meta.commentCount = (issue.comments || {}).totalCount || 0 63 | meta.postId = issue.id 64 | meta.score = score 65 | return meta 66 | } else return { postId: issue.id } 67 | } 68 | 69 | module.exports = issueToMeta 70 | -------------------------------------------------------------------------------- /lib/slug.js: -------------------------------------------------------------------------------- 1 | const _slug = require('slug') 2 | 3 | _slug.charmap['+'] = 'plus' 4 | 5 | const slug = (string, opts) => _slug(string, opts) 6 | 7 | module.exports = slug 8 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [[headers]] 2 | # Define which paths this specific [[headers]] block will cover. 3 | for = "/*" 4 | [headers.values] 5 | Access-Control-Allow-Origin = "*" 6 | -------------------------------------------------------------------------------- /out/folder: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stremio-addons-list", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "stremio-addons-list", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@octokit/graphql": "^4.5.2", 13 | "async.queue": "0.5.2", 14 | "dotenv": "^16.4.7", 15 | "needle": "3.1.0", 16 | "slug": "8.2.2" 17 | }, 18 | "engines": { 19 | "node": "16.18.0" 20 | } 21 | }, 22 | "node_modules/@octokit/endpoint": { 23 | "version": "6.0.12", 24 | "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", 25 | "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==", 26 | "dependencies": { 27 | "@octokit/types": "^6.0.3", 28 | "is-plain-object": "^5.0.0", 29 | "universal-user-agent": "^6.0.0" 30 | } 31 | }, 32 | "node_modules/@octokit/graphql": { 33 | "version": "4.5.2", 34 | "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.5.2.tgz", 35 | "integrity": "sha512-SpB/JGdB7bxRj8qowwfAXjMpICUYSJqRDj26MKJAryRQBqp/ZzARsaO2LEFWzDaps0FLQoPYVGppS0HQXkBhdg==", 36 | "dependencies": { 37 | "@octokit/request": "^5.3.0", 38 | "@octokit/types": "^5.0.0", 39 | "universal-user-agent": "^6.0.0" 40 | } 41 | }, 42 | "node_modules/@octokit/graphql/node_modules/@octokit/types": { 43 | "version": "5.5.0", 44 | "resolved": "https://registry.npmjs.org/@octokit/types/-/types-5.5.0.tgz", 45 | "integrity": "sha512-UZ1pErDue6bZNjYOotCNveTXArOMZQFG6hKJfOnGnulVCMcVVi7YIIuuR4WfBhjo7zgpmzn/BkPDnUXtNx+PcQ==", 46 | "dependencies": { 47 | "@types/node": ">= 8" 48 | } 49 | }, 50 | "node_modules/@octokit/openapi-types": { 51 | "version": "12.11.0", 52 | "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", 53 | "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==" 54 | }, 55 | "node_modules/@octokit/request": { 56 | "version": "5.6.3", 57 | "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz", 58 | "integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==", 59 | "dependencies": { 60 | "@octokit/endpoint": "^6.0.1", 61 | "@octokit/request-error": "^2.1.0", 62 | "@octokit/types": "^6.16.1", 63 | "is-plain-object": "^5.0.0", 64 | "node-fetch": "^2.6.7", 65 | "universal-user-agent": "^6.0.0" 66 | } 67 | }, 68 | "node_modules/@octokit/request-error": { 69 | "version": "2.1.0", 70 | "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", 71 | "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", 72 | "dependencies": { 73 | "@octokit/types": "^6.0.3", 74 | "deprecation": "^2.0.0", 75 | "once": "^1.4.0" 76 | } 77 | }, 78 | "node_modules/@octokit/types": { 79 | "version": "6.41.0", 80 | "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", 81 | "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", 82 | "dependencies": { 83 | "@octokit/openapi-types": "^12.11.0" 84 | } 85 | }, 86 | "node_modules/@types/node": { 87 | "version": "18.11.5", 88 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.5.tgz", 89 | "integrity": "sha512-3JRwhbjI+cHLAkUorhf8RnqUbFXajvzX4q6fMn5JwkgtuwfYtRQYI3u4V92vI6NJuTsbBQWWh3RZjFsuevyMGQ==" 90 | }, 91 | "node_modules/async.queue": { 92 | "version": "0.5.2", 93 | "resolved": "https://registry.npmjs.org/async.queue/-/async.queue-0.5.2.tgz", 94 | "integrity": "sha512-SX5gCWh47bIRLqHAt/zfJmRl2xpCF4OM4kkT3HehVJLmgQfLzYjL6QhLHe+SS4e7FOYU6NgMy2kXPB2wVchncg==", 95 | "dependencies": { 96 | "async.util.queue": "0.5.2" 97 | } 98 | }, 99 | "node_modules/async.util.arrayeach": { 100 | "version": "0.5.2", 101 | "resolved": "https://registry.npmjs.org/async.util.arrayeach/-/async.util.arrayeach-0.5.2.tgz", 102 | "integrity": "sha512-PIb4rVYjwzLqb93XX2wj0+mA9YTgSWtxQRKxtuLqxXvGj1xZMB6qJUfr1NhS+FSaMPJIE1tF40Gl/o/vlfIz/A==" 103 | }, 104 | "node_modules/async.util.isarray": { 105 | "version": "0.5.2", 106 | "resolved": "https://registry.npmjs.org/async.util.isarray/-/async.util.isarray-0.5.2.tgz", 107 | "integrity": "sha512-wbUzlrwON8RUgi+v/rhF0U99Ce8Osjcn+JP/mFNg6ymvShcobAOvE6cvLajSY5dPqKCOE1xfdhefgBif11zZgw==" 108 | }, 109 | "node_modules/async.util.map": { 110 | "version": "0.5.2", 111 | "resolved": "https://registry.npmjs.org/async.util.map/-/async.util.map-0.5.2.tgz", 112 | "integrity": "sha512-uXZhyNIH9Jo25jn35lUoEwFLAdZWC2ZQKjLO5PLq8VAisfW6qvSfgDLH4H57/WQSKZSo6OOmuqGhtdvIHDTi1Q==" 113 | }, 114 | "node_modules/async.util.noop": { 115 | "version": "0.5.2", 116 | "resolved": "https://registry.npmjs.org/async.util.noop/-/async.util.noop-0.5.2.tgz", 117 | "integrity": "sha512-AdwShXwE0KoskgqVJAck8zcM32nIHj3AC8ZN62ZaR5srhrY235Nw18vOJZWxcOfhxdVM0hRVKM8kMx7lcl7cCQ==" 118 | }, 119 | "node_modules/async.util.onlyonce": { 120 | "version": "0.5.2", 121 | "resolved": "https://registry.npmjs.org/async.util.onlyonce/-/async.util.onlyonce-0.5.2.tgz", 122 | "integrity": "sha512-UgQvkU9JZ+I0Cm1f56XyGXcII+J3d/5XWUuHpcevlItuA3WFSJcqZrsyAUck2FkRSD8BwYQX1zUTDp3SJMVESg==" 123 | }, 124 | "node_modules/async.util.queue": { 125 | "version": "0.5.2", 126 | "resolved": "https://registry.npmjs.org/async.util.queue/-/async.util.queue-0.5.2.tgz", 127 | "integrity": "sha512-DlKOFnhCzERL9D3bLKlNdgXwSckckcj+XkCvNuX4KMs4brBc2lHvPg8MK4NoPIhwAvUBGvE4NECdNRY0I5UOEQ==", 128 | "dependencies": { 129 | "async.util.arrayeach": "0.5.2", 130 | "async.util.isarray": "0.5.2", 131 | "async.util.map": "0.5.2", 132 | "async.util.noop": "0.5.2", 133 | "async.util.onlyonce": "0.5.2", 134 | "async.util.setimmediate": "0.5.2" 135 | } 136 | }, 137 | "node_modules/async.util.setimmediate": { 138 | "version": "0.5.2", 139 | "resolved": "https://registry.npmjs.org/async.util.setimmediate/-/async.util.setimmediate-0.5.2.tgz", 140 | "integrity": "sha512-aCYF85ZFCQ9Xn0106GcOVx+LvFguIIzfbfRTOlQoie3G4KeSjURfA6f7CfpFAF09FNP2A1MtdjeFdvYeTGDebw==" 141 | }, 142 | "node_modules/debug": { 143 | "version": "3.2.7", 144 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", 145 | "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", 146 | "dependencies": { 147 | "ms": "^2.1.1" 148 | } 149 | }, 150 | "node_modules/deprecation": { 151 | "version": "2.3.1", 152 | "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", 153 | "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" 154 | }, 155 | "node_modules/dotenv": { 156 | "version": "16.4.7", 157 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", 158 | "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", 159 | "engines": { 160 | "node": ">=12" 161 | }, 162 | "funding": { 163 | "url": "https://dotenvx.com" 164 | } 165 | }, 166 | "node_modules/iconv-lite": { 167 | "version": "0.6.3", 168 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 169 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 170 | "dependencies": { 171 | "safer-buffer": ">= 2.1.2 < 3.0.0" 172 | }, 173 | "engines": { 174 | "node": ">=0.10.0" 175 | } 176 | }, 177 | "node_modules/is-plain-object": { 178 | "version": "5.0.0", 179 | "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", 180 | "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", 181 | "engines": { 182 | "node": ">=0.10.0" 183 | } 184 | }, 185 | "node_modules/ms": { 186 | "version": "2.1.3", 187 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 188 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 189 | }, 190 | "node_modules/needle": { 191 | "version": "3.1.0", 192 | "resolved": "https://registry.npmjs.org/needle/-/needle-3.1.0.tgz", 193 | "integrity": "sha512-gCE9weDhjVGCRqS8dwDR/D3GTAeyXLXuqp7I8EzH6DllZGXSUyxuqqLh+YX9rMAWaaTFyVAg6rHGL25dqvczKw==", 194 | "dependencies": { 195 | "debug": "^3.2.6", 196 | "iconv-lite": "^0.6.3", 197 | "sax": "^1.2.4" 198 | }, 199 | "bin": { 200 | "needle": "bin/needle" 201 | }, 202 | "engines": { 203 | "node": ">= 4.4.x" 204 | } 205 | }, 206 | "node_modules/node-fetch": { 207 | "version": "2.6.7", 208 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", 209 | "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", 210 | "dependencies": { 211 | "whatwg-url": "^5.0.0" 212 | }, 213 | "engines": { 214 | "node": "4.x || >=6.0.0" 215 | }, 216 | "peerDependencies": { 217 | "encoding": "^0.1.0" 218 | }, 219 | "peerDependenciesMeta": { 220 | "encoding": { 221 | "optional": true 222 | } 223 | } 224 | }, 225 | "node_modules/once": { 226 | "version": "1.4.0", 227 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 228 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 229 | "dependencies": { 230 | "wrappy": "1" 231 | } 232 | }, 233 | "node_modules/safer-buffer": { 234 | "version": "2.1.2", 235 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 236 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 237 | }, 238 | "node_modules/sax": { 239 | "version": "1.2.4", 240 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", 241 | "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" 242 | }, 243 | "node_modules/slug": { 244 | "version": "8.2.2", 245 | "resolved": "https://registry.npmjs.org/slug/-/slug-8.2.2.tgz", 246 | "integrity": "sha512-5ByW6qXqPeG0Tmlkh24JhdXhvQsbaJSjVr3GgGxUV0BSskZKKBZZfFWxezap8+fh1vxBN9GVbqI1V6nqAFxlBg==", 247 | "bin": { 248 | "slug": "cli.js" 249 | } 250 | }, 251 | "node_modules/tr46": { 252 | "version": "0.0.3", 253 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 254 | "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" 255 | }, 256 | "node_modules/universal-user-agent": { 257 | "version": "6.0.0", 258 | "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", 259 | "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" 260 | }, 261 | "node_modules/webidl-conversions": { 262 | "version": "3.0.1", 263 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 264 | "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" 265 | }, 266 | "node_modules/whatwg-url": { 267 | "version": "5.0.0", 268 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 269 | "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", 270 | "dependencies": { 271 | "tr46": "~0.0.3", 272 | "webidl-conversions": "^3.0.0" 273 | } 274 | }, 275 | "node_modules/wrappy": { 276 | "version": "1.0.2", 277 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 278 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" 279 | } 280 | }, 281 | "dependencies": { 282 | "@octokit/endpoint": { 283 | "version": "6.0.12", 284 | "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", 285 | "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==", 286 | "requires": { 287 | "@octokit/types": "^6.0.3", 288 | "is-plain-object": "^5.0.0", 289 | "universal-user-agent": "^6.0.0" 290 | } 291 | }, 292 | "@octokit/graphql": { 293 | "version": "4.5.2", 294 | "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.5.2.tgz", 295 | "integrity": "sha512-SpB/JGdB7bxRj8qowwfAXjMpICUYSJqRDj26MKJAryRQBqp/ZzARsaO2LEFWzDaps0FLQoPYVGppS0HQXkBhdg==", 296 | "requires": { 297 | "@octokit/request": "^5.3.0", 298 | "@octokit/types": "^5.0.0", 299 | "universal-user-agent": "^6.0.0" 300 | }, 301 | "dependencies": { 302 | "@octokit/types": { 303 | "version": "5.5.0", 304 | "resolved": "https://registry.npmjs.org/@octokit/types/-/types-5.5.0.tgz", 305 | "integrity": "sha512-UZ1pErDue6bZNjYOotCNveTXArOMZQFG6hKJfOnGnulVCMcVVi7YIIuuR4WfBhjo7zgpmzn/BkPDnUXtNx+PcQ==", 306 | "requires": { 307 | "@types/node": ">= 8" 308 | } 309 | } 310 | } 311 | }, 312 | "@octokit/openapi-types": { 313 | "version": "12.11.0", 314 | "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", 315 | "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==" 316 | }, 317 | "@octokit/request": { 318 | "version": "5.6.3", 319 | "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz", 320 | "integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==", 321 | "requires": { 322 | "@octokit/endpoint": "^6.0.1", 323 | "@octokit/request-error": "^2.1.0", 324 | "@octokit/types": "^6.16.1", 325 | "is-plain-object": "^5.0.0", 326 | "node-fetch": "^2.6.7", 327 | "universal-user-agent": "^6.0.0" 328 | } 329 | }, 330 | "@octokit/request-error": { 331 | "version": "2.1.0", 332 | "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", 333 | "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", 334 | "requires": { 335 | "@octokit/types": "^6.0.3", 336 | "deprecation": "^2.0.0", 337 | "once": "^1.4.0" 338 | } 339 | }, 340 | "@octokit/types": { 341 | "version": "6.41.0", 342 | "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", 343 | "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", 344 | "requires": { 345 | "@octokit/openapi-types": "^12.11.0" 346 | } 347 | }, 348 | "@types/node": { 349 | "version": "18.11.5", 350 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.5.tgz", 351 | "integrity": "sha512-3JRwhbjI+cHLAkUorhf8RnqUbFXajvzX4q6fMn5JwkgtuwfYtRQYI3u4V92vI6NJuTsbBQWWh3RZjFsuevyMGQ==" 352 | }, 353 | "async.queue": { 354 | "version": "0.5.2", 355 | "resolved": "https://registry.npmjs.org/async.queue/-/async.queue-0.5.2.tgz", 356 | "integrity": "sha512-SX5gCWh47bIRLqHAt/zfJmRl2xpCF4OM4kkT3HehVJLmgQfLzYjL6QhLHe+SS4e7FOYU6NgMy2kXPB2wVchncg==", 357 | "requires": { 358 | "async.util.queue": "0.5.2" 359 | } 360 | }, 361 | "async.util.arrayeach": { 362 | "version": "0.5.2", 363 | "resolved": "https://registry.npmjs.org/async.util.arrayeach/-/async.util.arrayeach-0.5.2.tgz", 364 | "integrity": "sha512-PIb4rVYjwzLqb93XX2wj0+mA9YTgSWtxQRKxtuLqxXvGj1xZMB6qJUfr1NhS+FSaMPJIE1tF40Gl/o/vlfIz/A==" 365 | }, 366 | "async.util.isarray": { 367 | "version": "0.5.2", 368 | "resolved": "https://registry.npmjs.org/async.util.isarray/-/async.util.isarray-0.5.2.tgz", 369 | "integrity": "sha512-wbUzlrwON8RUgi+v/rhF0U99Ce8Osjcn+JP/mFNg6ymvShcobAOvE6cvLajSY5dPqKCOE1xfdhefgBif11zZgw==" 370 | }, 371 | "async.util.map": { 372 | "version": "0.5.2", 373 | "resolved": "https://registry.npmjs.org/async.util.map/-/async.util.map-0.5.2.tgz", 374 | "integrity": "sha512-uXZhyNIH9Jo25jn35lUoEwFLAdZWC2ZQKjLO5PLq8VAisfW6qvSfgDLH4H57/WQSKZSo6OOmuqGhtdvIHDTi1Q==" 375 | }, 376 | "async.util.noop": { 377 | "version": "0.5.2", 378 | "resolved": "https://registry.npmjs.org/async.util.noop/-/async.util.noop-0.5.2.tgz", 379 | "integrity": "sha512-AdwShXwE0KoskgqVJAck8zcM32nIHj3AC8ZN62ZaR5srhrY235Nw18vOJZWxcOfhxdVM0hRVKM8kMx7lcl7cCQ==" 380 | }, 381 | "async.util.onlyonce": { 382 | "version": "0.5.2", 383 | "resolved": "https://registry.npmjs.org/async.util.onlyonce/-/async.util.onlyonce-0.5.2.tgz", 384 | "integrity": "sha512-UgQvkU9JZ+I0Cm1f56XyGXcII+J3d/5XWUuHpcevlItuA3WFSJcqZrsyAUck2FkRSD8BwYQX1zUTDp3SJMVESg==" 385 | }, 386 | "async.util.queue": { 387 | "version": "0.5.2", 388 | "resolved": "https://registry.npmjs.org/async.util.queue/-/async.util.queue-0.5.2.tgz", 389 | "integrity": "sha512-DlKOFnhCzERL9D3bLKlNdgXwSckckcj+XkCvNuX4KMs4brBc2lHvPg8MK4NoPIhwAvUBGvE4NECdNRY0I5UOEQ==", 390 | "requires": { 391 | "async.util.arrayeach": "0.5.2", 392 | "async.util.isarray": "0.5.2", 393 | "async.util.map": "0.5.2", 394 | "async.util.noop": "0.5.2", 395 | "async.util.onlyonce": "0.5.2", 396 | "async.util.setimmediate": "0.5.2" 397 | } 398 | }, 399 | "async.util.setimmediate": { 400 | "version": "0.5.2", 401 | "resolved": "https://registry.npmjs.org/async.util.setimmediate/-/async.util.setimmediate-0.5.2.tgz", 402 | "integrity": "sha512-aCYF85ZFCQ9Xn0106GcOVx+LvFguIIzfbfRTOlQoie3G4KeSjURfA6f7CfpFAF09FNP2A1MtdjeFdvYeTGDebw==" 403 | }, 404 | "debug": { 405 | "version": "3.2.7", 406 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", 407 | "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", 408 | "requires": { 409 | "ms": "^2.1.1" 410 | } 411 | }, 412 | "deprecation": { 413 | "version": "2.3.1", 414 | "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", 415 | "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" 416 | }, 417 | "dotenv": { 418 | "version": "16.4.7", 419 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", 420 | "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==" 421 | }, 422 | "iconv-lite": { 423 | "version": "0.6.3", 424 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 425 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 426 | "requires": { 427 | "safer-buffer": ">= 2.1.2 < 3.0.0" 428 | } 429 | }, 430 | "is-plain-object": { 431 | "version": "5.0.0", 432 | "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", 433 | "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" 434 | }, 435 | "ms": { 436 | "version": "2.1.3", 437 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 438 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 439 | }, 440 | "needle": { 441 | "version": "3.1.0", 442 | "resolved": "https://registry.npmjs.org/needle/-/needle-3.1.0.tgz", 443 | "integrity": "sha512-gCE9weDhjVGCRqS8dwDR/D3GTAeyXLXuqp7I8EzH6DllZGXSUyxuqqLh+YX9rMAWaaTFyVAg6rHGL25dqvczKw==", 444 | "requires": { 445 | "debug": "^3.2.6", 446 | "iconv-lite": "^0.6.3", 447 | "sax": "^1.2.4" 448 | } 449 | }, 450 | "node-fetch": { 451 | "version": "2.6.7", 452 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", 453 | "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", 454 | "requires": { 455 | "whatwg-url": "^5.0.0" 456 | } 457 | }, 458 | "once": { 459 | "version": "1.4.0", 460 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 461 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 462 | "requires": { 463 | "wrappy": "1" 464 | } 465 | }, 466 | "safer-buffer": { 467 | "version": "2.1.2", 468 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 469 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 470 | }, 471 | "sax": { 472 | "version": "1.2.4", 473 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", 474 | "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" 475 | }, 476 | "slug": { 477 | "version": "8.2.2", 478 | "resolved": "https://registry.npmjs.org/slug/-/slug-8.2.2.tgz", 479 | "integrity": "sha512-5ByW6qXqPeG0Tmlkh24JhdXhvQsbaJSjVr3GgGxUV0BSskZKKBZZfFWxezap8+fh1vxBN9GVbqI1V6nqAFxlBg==" 480 | }, 481 | "tr46": { 482 | "version": "0.0.3", 483 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 484 | "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" 485 | }, 486 | "universal-user-agent": { 487 | "version": "6.0.0", 488 | "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", 489 | "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" 490 | }, 491 | "webidl-conversions": { 492 | "version": "3.0.1", 493 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 494 | "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" 495 | }, 496 | "whatwg-url": { 497 | "version": "5.0.0", 498 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 499 | "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", 500 | "requires": { 501 | "tr46": "~0.0.3", 502 | "webidl-conversions": "^3.0.0" 503 | } 504 | }, 505 | "wrappy": { 506 | "version": "1.0.2", 507 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 508 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" 509 | } 510 | } 511 | } 512 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stremio-addons-list", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "node build.js" 8 | }, 9 | "dependencies": { 10 | "@octokit/graphql": "^4.5.2", 11 | "async.queue": "0.5.2", 12 | "dotenv": "^16.4.7", 13 | "needle": "3.1.0", 14 | "slug": "8.2.2" 15 | }, 16 | "engines": { 17 | "node": "16.18.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /resources/isMobile.js: -------------------------------------------------------------------------------- 1 | window.isMobile = false 2 | if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent) 3 | || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0,4))) 4 | window.isMobile = true 5 | -------------------------------------------------------------------------------- /resources/isotope.3.0.6.pkgd.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Isotope PACKAGED v3.0.6 3 | * 4 | * Licensed GPLv3 for open source use 5 | * or Isotope Commercial License for commercial use 6 | * 7 | * https://isotope.metafizzy.co 8 | * Copyright 2010-2018 Metafizzy 9 | */ 10 | 11 | !function(t,e){"function"==typeof define&&define.amd?define("jquery-bridget/jquery-bridget",["jquery"],function(i){return e(t,i)}):"object"==typeof module&&module.exports?module.exports=e(t,require("jquery")):t.jQueryBridget=e(t,t.jQuery)}(window,function(t,e){"use strict";function i(i,s,a){function u(t,e,o){var n,s="$()."+i+'("'+e+'")';return t.each(function(t,u){var h=a.data(u,i);if(!h)return void r(i+" not initialized. Cannot call methods, i.e. "+s);var d=h[e];if(!d||"_"==e.charAt(0))return void r(s+" is not a valid method");var l=d.apply(h,o);n=void 0===n?l:n}),void 0!==n?n:t}function h(t,e){t.each(function(t,o){var n=a.data(o,i);n?(n.option(e),n._init()):(n=new s(o,e),a.data(o,i,n))})}a=a||e||t.jQuery,a&&(s.prototype.option||(s.prototype.option=function(t){a.isPlainObject(t)&&(this.options=a.extend(!0,this.options,t))}),a.fn[i]=function(t){if("string"==typeof t){var e=n.call(arguments,1);return u(this,t,e)}return h(this,t),this},o(a))}function o(t){!t||t&&t.bridget||(t.bridget=i)}var n=Array.prototype.slice,s=t.console,r="undefined"==typeof s?function(){}:function(t){s.error(t)};return o(e||t.jQuery),i}),function(t,e){"function"==typeof define&&define.amd?define("ev-emitter/ev-emitter",e):"object"==typeof module&&module.exports?module.exports=e():t.EvEmitter=e()}("undefined"!=typeof window?window:this,function(){function t(){}var e=t.prototype;return e.on=function(t,e){if(t&&e){var i=this._events=this._events||{},o=i[t]=i[t]||[];return o.indexOf(e)==-1&&o.push(e),this}},e.once=function(t,e){if(t&&e){this.on(t,e);var i=this._onceEvents=this._onceEvents||{},o=i[t]=i[t]||{};return o[e]=!0,this}},e.off=function(t,e){var i=this._events&&this._events[t];if(i&&i.length){var o=i.indexOf(e);return o!=-1&&i.splice(o,1),this}},e.emitEvent=function(t,e){var i=this._events&&this._events[t];if(i&&i.length){i=i.slice(0),e=e||[];for(var o=this._onceEvents&&this._onceEvents[t],n=0;n