├── .gitignore ├── LICENSE ├── README.md ├── images ├── cloudflare_create_a_worker.png ├── cloudflare_create_a_worker_success.png ├── cloudflare_create_an_application_workers.png ├── cloudflare_finished_worker_script.png ├── cloudflare_worker_editor.png ├── cloudflare_workers_pages_overview.png ├── google_auth_platform_0.png ├── google_auth_platform_1.png ├── google_auth_platform_clients_download.png ├── google_auth_platform_clients_download_menu.png ├── google_cloud_auth_platform_clients.png ├── google_cloud_auth_platform_clients_form.png ├── google_cloud_auth_platform_publish.png ├── google_cloud_create_project_1.png ├── google_cloud_create_project_2.png ├── google_cloud_create_project_3.png ├── google_cloud_create_project_4.png ├── google_cloud_gdrive_api.png ├── google_cloud_gdrive_api_search_results.png ├── google_cloud_oauth_search_results.png ├── google_cloud_onboarding.png └── stremio_gdrive_showcase.png ├── package-lock.json ├── package.json ├── src └── index.js └── wrangler.toml /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | 3 | logs 4 | _.log 5 | npm-debug.log_ 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | .pnpm-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | 13 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 14 | 15 | # Runtime data 16 | 17 | pids 18 | _.pid 19 | _.seed 20 | \*.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | 28 | coverage 29 | \*.lcov 30 | 31 | # nyc test coverage 32 | 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 36 | 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | 41 | bower_components 42 | 43 | # node-waf configuration 44 | 45 | .lock-wscript 46 | 47 | # Compiled binary addons (https://nodejs.org/api/addons.html) 48 | 49 | build/Release 50 | 51 | # Dependency directories 52 | 53 | node_modules/ 54 | jspm_packages/ 55 | 56 | # Snowpack dependency directory (https://snowpack.dev/) 57 | 58 | web_modules/ 59 | 60 | # TypeScript cache 61 | 62 | \*.tsbuildinfo 63 | 64 | # Optional npm cache directory 65 | 66 | .npm 67 | 68 | # Optional eslint cache 69 | 70 | .eslintcache 71 | 72 | # Optional stylelint cache 73 | 74 | .stylelintcache 75 | 76 | # Microbundle cache 77 | 78 | .rpt2_cache/ 79 | .rts2_cache_cjs/ 80 | .rts2_cache_es/ 81 | .rts2_cache_umd/ 82 | 83 | # Optional REPL history 84 | 85 | .node_repl_history 86 | 87 | # Output of 'npm pack' 88 | 89 | \*.tgz 90 | 91 | # Yarn Integrity file 92 | 93 | .yarn-integrity 94 | 95 | # dotenv environment variable files 96 | 97 | .env 98 | .env.development.local 99 | .env.test.local 100 | .env.production.local 101 | .env.local 102 | 103 | # parcel-bundler cache (https://parceljs.org/) 104 | 105 | .cache 106 | .parcel-cache 107 | 108 | # Next.js build output 109 | 110 | .next 111 | out 112 | 113 | # Nuxt.js build / generate output 114 | 115 | .nuxt 116 | dist 117 | 118 | # Gatsby files 119 | 120 | .cache/ 121 | 122 | # Comment in the public line in if your project uses Gatsby and not Next.js 123 | 124 | # https://nextjs.org/blog/next-9-1#public-directory-support 125 | 126 | # public 127 | 128 | # vuepress build output 129 | 130 | .vuepress/dist 131 | 132 | # vuepress v2.x temp and cache directory 133 | 134 | .temp 135 | .cache 136 | 137 | # Docusaurus cache and generated files 138 | 139 | .docusaurus 140 | 141 | # Serverless directories 142 | 143 | .serverless/ 144 | 145 | # FuseBox cache 146 | 147 | .fusebox/ 148 | 149 | # DynamoDB Local files 150 | 151 | .dynamodb/ 152 | 153 | # TernJS port file 154 | 155 | .tern-port 156 | 157 | # Stores VSCode versions used for testing VSCode extensions 158 | 159 | .vscode-test 160 | 161 | # yarn v2 162 | 163 | .yarn/cache 164 | .yarn/unplugged 165 | .yarn/build-state.yml 166 | .yarn/install-state.gz 167 | .pnp.\* 168 | 169 | # wrangler project 170 | 171 | .dev.vars 172 | .wrangler/ 173 | 174 | # editor files 175 | 176 | .vscode/ 177 | .prettierrc -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Viren070 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 | # Stremio GDrive Addon 2 | 3 | This is a simple addon for Stremio that allows you to watch videos from Google Drive. 4 | 5 | It searches your entire Google Drive and any shared drives you have access to for videos and presents them in Stremio. 6 | 7 | If you combine it with some team drives, you have loads of content, all available to watch for free and without torrenting. 8 | 9 | ![showcase](/images/stremio_gdrive_showcase.png) 10 | 11 | ## Features 12 | 13 | - Search your Google Drive and shared drives for videos 14 | - Parses filenames for information accurately using regex and displays it in a appealing format. 15 | - Catalog support - both search and full list on home page 16 | - Kitsu support. 17 | - TMDB Meta support if TMDB api key is provided. 18 | - Easily configurable using the `CONFIG` object at the top of the code. (See [Configuration](#configuration)) 19 | - Change the addon name 20 | - Change the order of resolutions, qualities, visual tags, and filter them out if unwanted 21 | - Change the sorting criteria (resolution, quality, size, visual tags) 22 | - Prioritise specific languages and have them show up first in the results 23 | - Only requires a single deployment with one file, making it easy to deploy and make changes. 24 | 25 | ## Deployment 26 | 27 | This addon is designed to be deployed as a worker on Cloudflare Workers. 28 | 29 | Here is a guide to deploying this addon, taken from my site: [Viren070's guides](https://guides.viren070.me/stremio/addons/stremio-gdrive). This will most likely be easier to follow on my site, however. 30 | 31 | 32 | ### Setting up our Google App 33 | 34 | 1. Go to the [Google Cloud Console](https://console.cloud.google.com/). 35 | 36 | If this is your first time using Google Cloud, you will be prompted to agree to the terms of service: 37 | 38 | ![Google Cloud Console](/images/google_cloud_onboarding.png) 39 | 40 | 2. Create a new project and select it: 41 | 42 |
43 | How? 44 | 45 | 1. Click on `Select a project` in the top left: 46 | 47 | ![Create a new project](/images/google_cloud_create_project_1.png) 48 | 49 | 2. Click on `New Project`: 50 | 51 | ![Create a new project](/images/google_cloud_create_project_2.png) 52 | 53 | 3. Enter a project name and click on `Create`: 54 | 55 | ![Create a new project](/images/google_cloud_create_project_3.png) 56 | 57 | - The project name can be anything you want, e.g., `Stremio-Gdrive`. Leave the `Organization` field blank. 58 | 59 | 4. Once the project has been created, you will get a notification: 60 | 61 | ![Create a new project](/images/google_cloud_create_project_4.png) 62 | 63 | - Click `Select Project`. 64 | 65 | :::note 66 | You may also use the same dropdown from step i to select the project. 67 | ::: 68 | 69 |
70 | 71 | 3. Setup our Google Auth Platform 72 | 73 | 1. Go to the [Google Cloud Console](https://console.cloud.google.com/). 74 | 2. In the search bar at the top, search for `Google Auth Platform` and click on the result: 75 | 76 | ![Google OAuth Platform](/images/google_cloud_oauth_search_results.png) 77 | 78 | 3. You should be met with a message telling you that `Google Auth Platform not configured yet`, click on `Get Started`: 79 | 80 | ![Google OAuth Platform](/images/google_auth_platform_0.png) 81 | 82 | 4. Fill in the form: 83 | 84 | ![Google OAuth Platform](/images/google_auth_platform_1.png) 85 | 86 | 1. `App Information`: 87 | - Set `App Name` to `Stremio GDrive`. 88 | - Set `User Support Email` to your email. It should appear in the dropdown. 89 | 2. `Audience`: 90 | - Set `User Type` to `External`. 91 | 3. `Contact Information` 92 | - Add any email address, you can use the same one you used earlier for `User Support Email`. 93 | 4. `Finish` 94 | - Check the box to agree to the `Google API Services: User Data Policy` 95 | 96 | 97 | 5. Once you have filled in the form, click on `Create` 98 | 99 | 4. Enable the Google Drive API. 100 | 101 | 1. Go to the [Google Cloud Console](https://console.cloud.google.com/). 102 | 2. In the search bar at the top, search for `Google Drive API` and click on the result: 103 | 104 | ![Google Drive API](/images/google_cloud_gdrive_api_search_results.png) 105 | 106 | 3. Click on `Enable`: 107 | 108 | ![Google Drive API](/images/google_cloud_gdrive_api.png) 109 | 110 | 5. Create an OAuth client. 111 | 112 | 1. Go back to the `Google Auth Platform` page. 113 | 2. Click on `Clients` in the sidebar and then click on `+ Create Client`: 114 | 115 | ![Google OAuth Platform](/images/google_cloud_auth_platform_clients.png) 116 | 117 | 3. Fill in the form: 118 | 119 | ![Google OAuth Platform](/images/google_cloud_auth_platform_clients_form.png) 120 | 121 | - `Application Type`: Set this to `Web application`. 122 | - `Name`: You can set this to anything such as `Stremio GDrive`. 123 | - `Authorized redirect URIs`: Set this to `https://guides.viren070.me/oauth/google/callback` 124 | 125 | 126 | 4. Click on `Create`. 127 | 128 | 6. Publish the app. 129 | 130 | 1. Go back to the `Google Auth Platform` page. 131 | 2. Click on `Audience` in the sidebar and then click on `Publish`: 132 | 133 | ![Google OAuth Platform](/images/google_cloud_auth_platform_publish.png) 134 | 135 | 136 | ### Setting up the Cloudflare Worker 137 | 138 | 1. Go to the [Cloudflare Workers](https://workers.cloudflare.com/) page and click `Log In` or `Sign Up` if you don't have an account. 139 | 140 | 2. Once logged in, you should be taken to the Cloudflare Workers & Pages dashboard. Click on `Create`: 141 | 142 | ![Cloudflare Workers](/images/cloudflare_workers_pages_overview.png) 143 | 144 | 3. Once on the create page, make sure you're on the `Workers` tab and click `Create Worker`: 145 | 146 | ![Cloudflare Workers](/images/cloudflare_create_an_application_workers.png) 147 | 148 | 4. You'll be asked to give a name to your worker. You can name it anything, this will be the URL you enter into Stremio to access your addon. Click `Deploy` once named. 149 | 150 | ![Cloudflare Workers](/images/cloudflare_create_a_worker.png) 151 | 152 | 5. Once its done being deployed, you should be shown a success message. Click the `Edit code` button: 153 | 154 | ![Cloudflare Workers](/images/cloudflare_create_a_worker_success.png) 155 | 156 | 6. You should be taken to the Cloudflare Worker editor: 157 | 158 | ![Cloudflare Workers](/images/cloudflare_worker_editor.png) 159 | 160 | 7. Now, we need to obtain the code for the addon that we will use specific to our Google Drive. 161 | First, we need to obtain our `Client ID` and `Client Secret` from the Google Cloud Console. 162 | 163 | 1. Go to the [Google Cloud Console](https://console.cloud.google.com/). 164 | 165 | 2. In the search bar at the top, search for `Google Auth Platform` and click on the result: 166 | 167 | ![Google OAuth Platform](/images/google_cloud_oauth_search_results.png) 168 | 169 | 3. Click on `Clients` and click on the download icon for the client you created earlier: 170 | 171 | ![Google OAuth Platform](/images/google_auth_platform_clients_download.png) 172 | 173 | 4. A pop-up will appear with your `Client ID` and `Client Secret`. 174 | 175 | ![Google OAuth Platform](/images/google_auth_platform_clients_download_menu.png) 176 | 177 | 5. You can click the copy icons to copy the `Client ID` and `Client Secret` to your clipboard for the next step. 178 | 179 | 8. Now, we can get the code for the Cloudflare Worker. 180 | 181 | 1. Go to the [OAuth Tool](https://guides.viren070.me/oauth/google) 182 | 183 | 2. Fill in the form with the `Client ID` and `Client Secret` from the previous step. 184 | 185 | 3. Click `Authorise` 186 | 187 | 4. Sign in with your Google account and allow the app to access your Google Drive. 188 | 189 | > You may encounter a warning page saying `Google hasn't verified this app`, click on `Advanced` and then `Go to... (unsafe)`. 190 | > 191 | > This warning is because the app is not verified by Google. This is normal for self-hosted apps. 192 | 193 | 5. You will be redirected back to the OAuth Tool with a success message. Click `Get Addon Code`. 194 | 195 | 6. You should be shown another success message. Then, make sure you're on the `Viren070` tab and you should see a block of code. Copy this code. 196 | 197 | 7. Go back to the Cloudflare Worker editor and after removing the existing code, paste the code you copied. 198 | 199 | 8. Your code should look something like this: 200 | 201 | ![Cloudflare Workers](/images/cloudflare_finished_worker_script.png) 202 | 203 | 10. Click `Deploy` in the top right to save your changes and deploy the worker. 204 | 205 | 11. Once deployed, you should see a green success message at the bottom. Click `Visit` next to the deploy button to go to the addon URL. 206 | 207 | 12. You should be redirected to the /manifest.json. if not, append `/manifest.json` to the URL in the address bar. 208 | 209 | 13. Copy the URL and [add it to Stremio](https://guides.viren070.me/stremio/faq#how-do-i-install-an-addon-manually). 210 | 211 | Done! You have now set up your own addon which will allow you to stream videos from your drives and team drives. 212 | 213 | ## Configuration 214 | 215 | Although you may modify the code as you wish, I have supplied a `CONFIG` object at the top of the code after `CREDENTIALS` that allows you to easily 216 | configure some aspects of the addon. 217 | 218 | > [!NOTE] 219 | > 220 | > Unless stated otherwise, all values are case sensitive 221 | 222 | This table explains the configuration options: 223 | 224 | | Name | Type | Values | Description | 225 | |:-------------------------------------: |:------------------: |-------------------------------------------------------------------------------------------------------------------------------------------------- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 226 | | `resolution` | `String[]` | `"2160p"`, `"1080p"`, `"720p"`, `"480p"`, `"Unknown"` | This setting allows you to configure which resolutions are shown in your results. You may also change the order of this to change the order that the resolutions would show in the addon (if you sort by resolutions)

You can remove certain resolutions to not have them show up in your results. The Unknown resolution occurs when the resolution could not be determined from the filename.

You cannot add new resolutions to this list unless you also add the corresponding regex in the `REGEX_PATTERNS` object. | 227 | | `qualities` | `String[]` | `"BluRay REMUX"`, `"BluRay"`, `"WEB-DL"`, `"WEBRip"`, `"HDRip"`, `"HC HD-Rip"`, `"DVDRip"`, `"HDTV"`, `"CAM"`, `"TS"`, `"TC"`, `"SCR"`, `"CAM"` | This setting allows you to configure which qualities are shown in your results. You may also change the order of this list to change the priority of them when sorting by quality. (e.g. if you put CAM/TS at the top of the list and sorted by quality, then CAM/TS results would appear higher than other qualities)

Remove qualities from the list to remove them from your results. The Unknown quality occurs when one of the existing qualities could not be found in the filename.

You cannot add new qualities to this list unless you also add the corresponding regex in the `REGEX_PATTERNS` object. | 228 | | `visualTags` | `String[]` | `"HDR10+"`, `"HDR10"`, `"HDR"`, `"DV"`, `"IMAX"`, `"AI"` | This setting allows you to configure which visualTags are shown in your results. You can also change the order of this to change the priority of each visual tag when sorting by `visualTag` (e.g. If you were sorting by visualTag and moved IMAX to the front, and removed AI, then any results with the AI tag will be removed and any results with the IMAX tag will be pushed to the front)

You cannot add new visual tags to this list unless you also add the corresponding regex in the `REGEX_PATTERNS` object. | 229 | | `sortBy` | `String[]` | `"resolution"`, `"quality"`, `"size"`, `"visualTag"` | Change the order of this list to change the priority for which the results are sorted by.

`resolution` - sort by the resolution of the file e.g. 1080p, 720p. The better the resolution (determined by the resolution's position in the `resolution` list) the higher the result.
`quality` - sort by the quality of the file, e.g. BluRay, WEBRip. The better the quality (determined by the quality's position in the `qualities` list) the higher the result.
`size` - sort by the size of file e.g. 12GB. The higher the size the higher they show in the results.
`visualTag` - sort by the priority of the visual tags. Files with a visual tag are sorted higher and between files that both have visual tags, the order of the visual tag in the `visualTags` list will determine their order.

Examples:

If you want all 2160p results to show first with those results being sorted by size, then do resolution, then size
If you want results to be sorted by size regardless of resolution, only have size in the list.
If you want to see all HDR results first, and sort those results and the rest by size, then put `visualTag` and `size` in the list. | 230 | | `considerHdrTagsAsEqual` | `boolean` | `true`, `false` | When sorting by visualTag, if this value is set to false, the HDR tags (HDR, HDR10, HDR10+) will be considered differently, and depending on their position in the visualTags list, they will be sorted accordingly.

For example, if HDR10+ was placed first, then all HDR10+ files will appear first, regardless of if there are HDR files that rank higher based on other factors.

With this value set to true, all HDR tags are considered equal and if you were sorting by `visualTag`, then `size`, a HDR file could appear above a HDR10+ file if the size of the HDR file was greater. | 231 | | `addonName` | `String` | any | Change the value contained in this string to change the name of the addon that appears in Stremio in the addon list
and in the stream results. | 232 | | `prioritiseLanguage` | `String` \| `null` | See the languages object in the `REGEX_PATTERNS` object for a full list.

Set to `null` to disable prioritising specific language results. | By setting a prioritised language, you are pushing any results that have that language to the top.

However, parsed information about languages may be incorrect and filenames do not contain the language
that is contained within the file sometimes. | 233 | | `proxiedPlayback` | `boolean` | `true`, `false` | With `proxiedPlayback` enabled, the file will be streamed through the addon. If it is disabled, Stremio will stream directly from Google Drive.

If this option is disabled, streaming will not work on Stremio Web or through external players on iOS. You are also exposing your access token in the addon responses.

However, this option is experimental and may cause issues.

I recommend leaving it enabled, and only if you encounter issues, try disabling this option.

Note, that even with this enabled, anyone with your addon URL can still view your Google Drive files. | 234 | | `driveQueryTerms.`
`episodeFormat` | `String` | `"name"`, `"fullText"` (see the Drive v3 API for more) | This setting changes the object that we perform the queries upon for the episode formats (s01e03).

I recommend leaving this to fullText. However, if you are getting incorrect matches, try switching to name | 235 | | `driveQueryTerms.`
`movieYear` | `String` | `"name"`, `"fullText"` | This setting changes the object that we perform queries upon for the release year of the movie.

I recommend leaving this to name. However, if you are getting incorrect matches, try switching to fullText. | 236 | -------------------------------------------------------------------------------- /images/cloudflare_create_a_worker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Viren070/stremio-gdrive-addon/8945ed79f16b2001b48cda5372def91c68b4c806/images/cloudflare_create_a_worker.png -------------------------------------------------------------------------------- /images/cloudflare_create_a_worker_success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Viren070/stremio-gdrive-addon/8945ed79f16b2001b48cda5372def91c68b4c806/images/cloudflare_create_a_worker_success.png -------------------------------------------------------------------------------- /images/cloudflare_create_an_application_workers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Viren070/stremio-gdrive-addon/8945ed79f16b2001b48cda5372def91c68b4c806/images/cloudflare_create_an_application_workers.png -------------------------------------------------------------------------------- /images/cloudflare_finished_worker_script.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Viren070/stremio-gdrive-addon/8945ed79f16b2001b48cda5372def91c68b4c806/images/cloudflare_finished_worker_script.png -------------------------------------------------------------------------------- /images/cloudflare_worker_editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Viren070/stremio-gdrive-addon/8945ed79f16b2001b48cda5372def91c68b4c806/images/cloudflare_worker_editor.png -------------------------------------------------------------------------------- /images/cloudflare_workers_pages_overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Viren070/stremio-gdrive-addon/8945ed79f16b2001b48cda5372def91c68b4c806/images/cloudflare_workers_pages_overview.png -------------------------------------------------------------------------------- /images/google_auth_platform_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Viren070/stremio-gdrive-addon/8945ed79f16b2001b48cda5372def91c68b4c806/images/google_auth_platform_0.png -------------------------------------------------------------------------------- /images/google_auth_platform_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Viren070/stremio-gdrive-addon/8945ed79f16b2001b48cda5372def91c68b4c806/images/google_auth_platform_1.png -------------------------------------------------------------------------------- /images/google_auth_platform_clients_download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Viren070/stremio-gdrive-addon/8945ed79f16b2001b48cda5372def91c68b4c806/images/google_auth_platform_clients_download.png -------------------------------------------------------------------------------- /images/google_auth_platform_clients_download_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Viren070/stremio-gdrive-addon/8945ed79f16b2001b48cda5372def91c68b4c806/images/google_auth_platform_clients_download_menu.png -------------------------------------------------------------------------------- /images/google_cloud_auth_platform_clients.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Viren070/stremio-gdrive-addon/8945ed79f16b2001b48cda5372def91c68b4c806/images/google_cloud_auth_platform_clients.png -------------------------------------------------------------------------------- /images/google_cloud_auth_platform_clients_form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Viren070/stremio-gdrive-addon/8945ed79f16b2001b48cda5372def91c68b4c806/images/google_cloud_auth_platform_clients_form.png -------------------------------------------------------------------------------- /images/google_cloud_auth_platform_publish.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Viren070/stremio-gdrive-addon/8945ed79f16b2001b48cda5372def91c68b4c806/images/google_cloud_auth_platform_publish.png -------------------------------------------------------------------------------- /images/google_cloud_create_project_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Viren070/stremio-gdrive-addon/8945ed79f16b2001b48cda5372def91c68b4c806/images/google_cloud_create_project_1.png -------------------------------------------------------------------------------- /images/google_cloud_create_project_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Viren070/stremio-gdrive-addon/8945ed79f16b2001b48cda5372def91c68b4c806/images/google_cloud_create_project_2.png -------------------------------------------------------------------------------- /images/google_cloud_create_project_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Viren070/stremio-gdrive-addon/8945ed79f16b2001b48cda5372def91c68b4c806/images/google_cloud_create_project_3.png -------------------------------------------------------------------------------- /images/google_cloud_create_project_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Viren070/stremio-gdrive-addon/8945ed79f16b2001b48cda5372def91c68b4c806/images/google_cloud_create_project_4.png -------------------------------------------------------------------------------- /images/google_cloud_gdrive_api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Viren070/stremio-gdrive-addon/8945ed79f16b2001b48cda5372def91c68b4c806/images/google_cloud_gdrive_api.png -------------------------------------------------------------------------------- /images/google_cloud_gdrive_api_search_results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Viren070/stremio-gdrive-addon/8945ed79f16b2001b48cda5372def91c68b4c806/images/google_cloud_gdrive_api_search_results.png -------------------------------------------------------------------------------- /images/google_cloud_oauth_search_results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Viren070/stremio-gdrive-addon/8945ed79f16b2001b48cda5372def91c68b4c806/images/google_cloud_oauth_search_results.png -------------------------------------------------------------------------------- /images/google_cloud_onboarding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Viren070/stremio-gdrive-addon/8945ed79f16b2001b48cda5372def91c68b4c806/images/google_cloud_onboarding.png -------------------------------------------------------------------------------- /images/stremio_gdrive_showcase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Viren070/stremio-gdrive-addon/8945ed79f16b2001b48cda5372def91c68b4c806/images/stremio_gdrive_showcase.png -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stremio-gdrive", 3 | "version": "0.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "stremio-gdrive", 9 | "version": "0.0.0", 10 | "devDependencies": { 11 | "wrangler": "^3.60.3" 12 | } 13 | }, 14 | "node_modules/@cloudflare/kv-asset-handler": { 15 | "version": "0.3.4", 16 | "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.4.tgz", 17 | "integrity": "sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==", 18 | "dev": true, 19 | "license": "MIT OR Apache-2.0", 20 | "dependencies": { 21 | "mime": "^3.0.0" 22 | }, 23 | "engines": { 24 | "node": ">=16.13" 25 | } 26 | }, 27 | "node_modules/@cloudflare/workerd-darwin-64": { 28 | "version": "1.20241205.0", 29 | "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20241205.0.tgz", 30 | "integrity": "sha512-TArEZkSZkHJyEwnlWWkSpCI99cF6lJ14OVeEoI9Um/+cD9CKZLM9vCmsLeKglKheJ0KcdCnkA+DbeD15t3VaWg==", 31 | "cpu": [ 32 | "x64" 33 | ], 34 | "dev": true, 35 | "license": "Apache-2.0", 36 | "optional": true, 37 | "os": [ 38 | "darwin" 39 | ], 40 | "engines": { 41 | "node": ">=16" 42 | } 43 | }, 44 | "node_modules/@cloudflare/workerd-darwin-arm64": { 45 | "version": "1.20241205.0", 46 | "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20241205.0.tgz", 47 | "integrity": "sha512-u5eqKa9QRdA8MugfgCoD+ADDjY6EpKbv3hSYJETmmUh17l7WXjWBzv4pUvOKIX67C0UzMUy4jZYwC53MymhX3w==", 48 | "cpu": [ 49 | "arm64" 50 | ], 51 | "dev": true, 52 | "license": "Apache-2.0", 53 | "optional": true, 54 | "os": [ 55 | "darwin" 56 | ], 57 | "engines": { 58 | "node": ">=16" 59 | } 60 | }, 61 | "node_modules/@cloudflare/workerd-linux-64": { 62 | "version": "1.20241205.0", 63 | "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20241205.0.tgz", 64 | "integrity": "sha512-OYA7S5zpumMamWEW+IhhBU6YojIEocyE5X/YFPiTOCrDE3dsfr9t6oqNE7hxGm1VAAu+Irtl+a/5LwmBOU681w==", 65 | "cpu": [ 66 | "x64" 67 | ], 68 | "dev": true, 69 | "license": "Apache-2.0", 70 | "optional": true, 71 | "os": [ 72 | "linux" 73 | ], 74 | "engines": { 75 | "node": ">=16" 76 | } 77 | }, 78 | "node_modules/@cloudflare/workerd-linux-arm64": { 79 | "version": "1.20241205.0", 80 | "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20241205.0.tgz", 81 | "integrity": "sha512-qAzecONjFJGIAVJZKExQ5dlbic0f3d4A+GdKa+H6SoUJtPaWiE3K6WuePo4JOT7W3/Zfh25McmX+MmpMUUcM5Q==", 82 | "cpu": [ 83 | "arm64" 84 | ], 85 | "dev": true, 86 | "license": "Apache-2.0", 87 | "optional": true, 88 | "os": [ 89 | "linux" 90 | ], 91 | "engines": { 92 | "node": ">=16" 93 | } 94 | }, 95 | "node_modules/@cloudflare/workerd-windows-64": { 96 | "version": "1.20241205.0", 97 | "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20241205.0.tgz", 98 | "integrity": "sha512-BEab+HiUgCdl6GXAT7EI2yaRtDPiRJlB94XLvRvXi1ZcmQqsrq6awGo6apctFo4WUL29V7c09LxmN4HQ3X2Tvg==", 99 | "cpu": [ 100 | "x64" 101 | ], 102 | "dev": true, 103 | "license": "Apache-2.0", 104 | "optional": true, 105 | "os": [ 106 | "win32" 107 | ], 108 | "engines": { 109 | "node": ">=16" 110 | } 111 | }, 112 | "node_modules/@cloudflare/workers-shared": { 113 | "version": "0.11.0", 114 | "resolved": "https://registry.npmjs.org/@cloudflare/workers-shared/-/workers-shared-0.11.0.tgz", 115 | "integrity": "sha512-A+lQ8xp7992qSeMmuQ0ssL6CPmm+ZmAv6Ddikan0n1jjpMAic+97l7xtVIsswSn9iLMFPYQ9uNN/8Fl0AgARIQ==", 116 | "dev": true, 117 | "license": "MIT OR Apache-2.0", 118 | "dependencies": { 119 | "mime": "^3.0.0", 120 | "zod": "^3.22.3" 121 | }, 122 | "engines": { 123 | "node": ">=16.7.0" 124 | } 125 | }, 126 | "node_modules/@cspotcode/source-map-support": { 127 | "version": "0.8.1", 128 | "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", 129 | "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", 130 | "dev": true, 131 | "license": "MIT", 132 | "dependencies": { 133 | "@jridgewell/trace-mapping": "0.3.9" 134 | }, 135 | "engines": { 136 | "node": ">=12" 137 | } 138 | }, 139 | "node_modules/@esbuild-plugins/node-globals-polyfill": { 140 | "version": "0.2.3", 141 | "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-globals-polyfill/-/node-globals-polyfill-0.2.3.tgz", 142 | "integrity": "sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==", 143 | "dev": true, 144 | "license": "ISC", 145 | "peerDependencies": { 146 | "esbuild": "*" 147 | } 148 | }, 149 | "node_modules/@esbuild-plugins/node-modules-polyfill": { 150 | "version": "0.2.2", 151 | "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-modules-polyfill/-/node-modules-polyfill-0.2.2.tgz", 152 | "integrity": "sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==", 153 | "dev": true, 154 | "license": "ISC", 155 | "dependencies": { 156 | "escape-string-regexp": "^4.0.0", 157 | "rollup-plugin-node-polyfills": "^0.2.1" 158 | }, 159 | "peerDependencies": { 160 | "esbuild": "*" 161 | } 162 | }, 163 | "node_modules/@esbuild/android-arm": { 164 | "version": "0.17.19", 165 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", 166 | "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", 167 | "cpu": [ 168 | "arm" 169 | ], 170 | "dev": true, 171 | "license": "MIT", 172 | "optional": true, 173 | "os": [ 174 | "android" 175 | ], 176 | "engines": { 177 | "node": ">=12" 178 | } 179 | }, 180 | "node_modules/@esbuild/android-arm64": { 181 | "version": "0.17.19", 182 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", 183 | "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", 184 | "cpu": [ 185 | "arm64" 186 | ], 187 | "dev": true, 188 | "license": "MIT", 189 | "optional": true, 190 | "os": [ 191 | "android" 192 | ], 193 | "engines": { 194 | "node": ">=12" 195 | } 196 | }, 197 | "node_modules/@esbuild/android-x64": { 198 | "version": "0.17.19", 199 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", 200 | "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", 201 | "cpu": [ 202 | "x64" 203 | ], 204 | "dev": true, 205 | "license": "MIT", 206 | "optional": true, 207 | "os": [ 208 | "android" 209 | ], 210 | "engines": { 211 | "node": ">=12" 212 | } 213 | }, 214 | "node_modules/@esbuild/darwin-arm64": { 215 | "version": "0.17.19", 216 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", 217 | "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", 218 | "cpu": [ 219 | "arm64" 220 | ], 221 | "dev": true, 222 | "license": "MIT", 223 | "optional": true, 224 | "os": [ 225 | "darwin" 226 | ], 227 | "engines": { 228 | "node": ">=12" 229 | } 230 | }, 231 | "node_modules/@esbuild/darwin-x64": { 232 | "version": "0.17.19", 233 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", 234 | "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", 235 | "cpu": [ 236 | "x64" 237 | ], 238 | "dev": true, 239 | "license": "MIT", 240 | "optional": true, 241 | "os": [ 242 | "darwin" 243 | ], 244 | "engines": { 245 | "node": ">=12" 246 | } 247 | }, 248 | "node_modules/@esbuild/freebsd-arm64": { 249 | "version": "0.17.19", 250 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", 251 | "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", 252 | "cpu": [ 253 | "arm64" 254 | ], 255 | "dev": true, 256 | "license": "MIT", 257 | "optional": true, 258 | "os": [ 259 | "freebsd" 260 | ], 261 | "engines": { 262 | "node": ">=12" 263 | } 264 | }, 265 | "node_modules/@esbuild/freebsd-x64": { 266 | "version": "0.17.19", 267 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", 268 | "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", 269 | "cpu": [ 270 | "x64" 271 | ], 272 | "dev": true, 273 | "license": "MIT", 274 | "optional": true, 275 | "os": [ 276 | "freebsd" 277 | ], 278 | "engines": { 279 | "node": ">=12" 280 | } 281 | }, 282 | "node_modules/@esbuild/linux-arm": { 283 | "version": "0.17.19", 284 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", 285 | "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", 286 | "cpu": [ 287 | "arm" 288 | ], 289 | "dev": true, 290 | "license": "MIT", 291 | "optional": true, 292 | "os": [ 293 | "linux" 294 | ], 295 | "engines": { 296 | "node": ">=12" 297 | } 298 | }, 299 | "node_modules/@esbuild/linux-arm64": { 300 | "version": "0.17.19", 301 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", 302 | "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", 303 | "cpu": [ 304 | "arm64" 305 | ], 306 | "dev": true, 307 | "license": "MIT", 308 | "optional": true, 309 | "os": [ 310 | "linux" 311 | ], 312 | "engines": { 313 | "node": ">=12" 314 | } 315 | }, 316 | "node_modules/@esbuild/linux-ia32": { 317 | "version": "0.17.19", 318 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", 319 | "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", 320 | "cpu": [ 321 | "ia32" 322 | ], 323 | "dev": true, 324 | "license": "MIT", 325 | "optional": true, 326 | "os": [ 327 | "linux" 328 | ], 329 | "engines": { 330 | "node": ">=12" 331 | } 332 | }, 333 | "node_modules/@esbuild/linux-loong64": { 334 | "version": "0.17.19", 335 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", 336 | "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", 337 | "cpu": [ 338 | "loong64" 339 | ], 340 | "dev": true, 341 | "license": "MIT", 342 | "optional": true, 343 | "os": [ 344 | "linux" 345 | ], 346 | "engines": { 347 | "node": ">=12" 348 | } 349 | }, 350 | "node_modules/@esbuild/linux-mips64el": { 351 | "version": "0.17.19", 352 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", 353 | "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", 354 | "cpu": [ 355 | "mips64el" 356 | ], 357 | "dev": true, 358 | "license": "MIT", 359 | "optional": true, 360 | "os": [ 361 | "linux" 362 | ], 363 | "engines": { 364 | "node": ">=12" 365 | } 366 | }, 367 | "node_modules/@esbuild/linux-ppc64": { 368 | "version": "0.17.19", 369 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", 370 | "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", 371 | "cpu": [ 372 | "ppc64" 373 | ], 374 | "dev": true, 375 | "license": "MIT", 376 | "optional": true, 377 | "os": [ 378 | "linux" 379 | ], 380 | "engines": { 381 | "node": ">=12" 382 | } 383 | }, 384 | "node_modules/@esbuild/linux-riscv64": { 385 | "version": "0.17.19", 386 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", 387 | "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", 388 | "cpu": [ 389 | "riscv64" 390 | ], 391 | "dev": true, 392 | "license": "MIT", 393 | "optional": true, 394 | "os": [ 395 | "linux" 396 | ], 397 | "engines": { 398 | "node": ">=12" 399 | } 400 | }, 401 | "node_modules/@esbuild/linux-s390x": { 402 | "version": "0.17.19", 403 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", 404 | "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", 405 | "cpu": [ 406 | "s390x" 407 | ], 408 | "dev": true, 409 | "license": "MIT", 410 | "optional": true, 411 | "os": [ 412 | "linux" 413 | ], 414 | "engines": { 415 | "node": ">=12" 416 | } 417 | }, 418 | "node_modules/@esbuild/linux-x64": { 419 | "version": "0.17.19", 420 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", 421 | "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", 422 | "cpu": [ 423 | "x64" 424 | ], 425 | "dev": true, 426 | "license": "MIT", 427 | "optional": true, 428 | "os": [ 429 | "linux" 430 | ], 431 | "engines": { 432 | "node": ">=12" 433 | } 434 | }, 435 | "node_modules/@esbuild/netbsd-x64": { 436 | "version": "0.17.19", 437 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", 438 | "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", 439 | "cpu": [ 440 | "x64" 441 | ], 442 | "dev": true, 443 | "license": "MIT", 444 | "optional": true, 445 | "os": [ 446 | "netbsd" 447 | ], 448 | "engines": { 449 | "node": ">=12" 450 | } 451 | }, 452 | "node_modules/@esbuild/openbsd-x64": { 453 | "version": "0.17.19", 454 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", 455 | "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", 456 | "cpu": [ 457 | "x64" 458 | ], 459 | "dev": true, 460 | "license": "MIT", 461 | "optional": true, 462 | "os": [ 463 | "openbsd" 464 | ], 465 | "engines": { 466 | "node": ">=12" 467 | } 468 | }, 469 | "node_modules/@esbuild/sunos-x64": { 470 | "version": "0.17.19", 471 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", 472 | "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", 473 | "cpu": [ 474 | "x64" 475 | ], 476 | "dev": true, 477 | "license": "MIT", 478 | "optional": true, 479 | "os": [ 480 | "sunos" 481 | ], 482 | "engines": { 483 | "node": ">=12" 484 | } 485 | }, 486 | "node_modules/@esbuild/win32-arm64": { 487 | "version": "0.17.19", 488 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", 489 | "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", 490 | "cpu": [ 491 | "arm64" 492 | ], 493 | "dev": true, 494 | "license": "MIT", 495 | "optional": true, 496 | "os": [ 497 | "win32" 498 | ], 499 | "engines": { 500 | "node": ">=12" 501 | } 502 | }, 503 | "node_modules/@esbuild/win32-ia32": { 504 | "version": "0.17.19", 505 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", 506 | "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", 507 | "cpu": [ 508 | "ia32" 509 | ], 510 | "dev": true, 511 | "license": "MIT", 512 | "optional": true, 513 | "os": [ 514 | "win32" 515 | ], 516 | "engines": { 517 | "node": ">=12" 518 | } 519 | }, 520 | "node_modules/@esbuild/win32-x64": { 521 | "version": "0.17.19", 522 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", 523 | "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", 524 | "cpu": [ 525 | "x64" 526 | ], 527 | "dev": true, 528 | "license": "MIT", 529 | "optional": true, 530 | "os": [ 531 | "win32" 532 | ], 533 | "engines": { 534 | "node": ">=12" 535 | } 536 | }, 537 | "node_modules/@fastify/busboy": { 538 | "version": "2.1.1", 539 | "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", 540 | "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", 541 | "dev": true, 542 | "license": "MIT", 543 | "engines": { 544 | "node": ">=14" 545 | } 546 | }, 547 | "node_modules/@jridgewell/resolve-uri": { 548 | "version": "3.1.2", 549 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 550 | "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 551 | "dev": true, 552 | "license": "MIT", 553 | "engines": { 554 | "node": ">=6.0.0" 555 | } 556 | }, 557 | "node_modules/@jridgewell/sourcemap-codec": { 558 | "version": "1.5.0", 559 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", 560 | "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", 561 | "dev": true, 562 | "license": "MIT" 563 | }, 564 | "node_modules/@jridgewell/trace-mapping": { 565 | "version": "0.3.9", 566 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", 567 | "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", 568 | "dev": true, 569 | "license": "MIT", 570 | "dependencies": { 571 | "@jridgewell/resolve-uri": "^3.0.3", 572 | "@jridgewell/sourcemap-codec": "^1.4.10" 573 | } 574 | }, 575 | "node_modules/@types/node": { 576 | "version": "22.10.2", 577 | "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", 578 | "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", 579 | "dev": true, 580 | "license": "MIT", 581 | "dependencies": { 582 | "undici-types": "~6.20.0" 583 | } 584 | }, 585 | "node_modules/@types/node-forge": { 586 | "version": "1.3.11", 587 | "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", 588 | "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", 589 | "dev": true, 590 | "license": "MIT", 591 | "dependencies": { 592 | "@types/node": "*" 593 | } 594 | }, 595 | "node_modules/acorn": { 596 | "version": "8.14.0", 597 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", 598 | "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", 599 | "dev": true, 600 | "license": "MIT", 601 | "bin": { 602 | "acorn": "bin/acorn" 603 | }, 604 | "engines": { 605 | "node": ">=0.4.0" 606 | } 607 | }, 608 | "node_modules/acorn-walk": { 609 | "version": "8.3.4", 610 | "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", 611 | "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", 612 | "dev": true, 613 | "license": "MIT", 614 | "dependencies": { 615 | "acorn": "^8.11.0" 616 | }, 617 | "engines": { 618 | "node": ">=0.4.0" 619 | } 620 | }, 621 | "node_modules/as-table": { 622 | "version": "1.0.55", 623 | "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", 624 | "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", 625 | "dev": true, 626 | "license": "MIT", 627 | "dependencies": { 628 | "printable-characters": "^1.0.42" 629 | } 630 | }, 631 | "node_modules/blake3-wasm": { 632 | "version": "2.1.5", 633 | "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", 634 | "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", 635 | "dev": true, 636 | "license": "MIT" 637 | }, 638 | "node_modules/capnp-ts": { 639 | "version": "0.7.0", 640 | "resolved": "https://registry.npmjs.org/capnp-ts/-/capnp-ts-0.7.0.tgz", 641 | "integrity": "sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g==", 642 | "dev": true, 643 | "license": "MIT", 644 | "dependencies": { 645 | "debug": "^4.3.1", 646 | "tslib": "^2.2.0" 647 | } 648 | }, 649 | "node_modules/chokidar": { 650 | "version": "4.0.1", 651 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", 652 | "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", 653 | "dev": true, 654 | "license": "MIT", 655 | "dependencies": { 656 | "readdirp": "^4.0.1" 657 | }, 658 | "engines": { 659 | "node": ">= 14.16.0" 660 | }, 661 | "funding": { 662 | "url": "https://paulmillr.com/funding/" 663 | } 664 | }, 665 | "node_modules/cookie": { 666 | "version": "0.7.2", 667 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", 668 | "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", 669 | "dev": true, 670 | "license": "MIT", 671 | "engines": { 672 | "node": ">= 0.6" 673 | } 674 | }, 675 | "node_modules/data-uri-to-buffer": { 676 | "version": "2.0.2", 677 | "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", 678 | "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", 679 | "dev": true, 680 | "license": "MIT" 681 | }, 682 | "node_modules/date-fns": { 683 | "version": "4.1.0", 684 | "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", 685 | "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", 686 | "dev": true, 687 | "license": "MIT", 688 | "funding": { 689 | "type": "github", 690 | "url": "https://github.com/sponsors/kossnocorp" 691 | } 692 | }, 693 | "node_modules/debug": { 694 | "version": "4.4.0", 695 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", 696 | "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", 697 | "dev": true, 698 | "license": "MIT", 699 | "dependencies": { 700 | "ms": "^2.1.3" 701 | }, 702 | "engines": { 703 | "node": ">=6.0" 704 | }, 705 | "peerDependenciesMeta": { 706 | "supports-color": { 707 | "optional": true 708 | } 709 | } 710 | }, 711 | "node_modules/defu": { 712 | "version": "6.1.4", 713 | "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", 714 | "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", 715 | "dev": true, 716 | "license": "MIT" 717 | }, 718 | "node_modules/esbuild": { 719 | "version": "0.17.19", 720 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", 721 | "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", 722 | "dev": true, 723 | "hasInstallScript": true, 724 | "license": "MIT", 725 | "bin": { 726 | "esbuild": "bin/esbuild" 727 | }, 728 | "engines": { 729 | "node": ">=12" 730 | }, 731 | "optionalDependencies": { 732 | "@esbuild/android-arm": "0.17.19", 733 | "@esbuild/android-arm64": "0.17.19", 734 | "@esbuild/android-x64": "0.17.19", 735 | "@esbuild/darwin-arm64": "0.17.19", 736 | "@esbuild/darwin-x64": "0.17.19", 737 | "@esbuild/freebsd-arm64": "0.17.19", 738 | "@esbuild/freebsd-x64": "0.17.19", 739 | "@esbuild/linux-arm": "0.17.19", 740 | "@esbuild/linux-arm64": "0.17.19", 741 | "@esbuild/linux-ia32": "0.17.19", 742 | "@esbuild/linux-loong64": "0.17.19", 743 | "@esbuild/linux-mips64el": "0.17.19", 744 | "@esbuild/linux-ppc64": "0.17.19", 745 | "@esbuild/linux-riscv64": "0.17.19", 746 | "@esbuild/linux-s390x": "0.17.19", 747 | "@esbuild/linux-x64": "0.17.19", 748 | "@esbuild/netbsd-x64": "0.17.19", 749 | "@esbuild/openbsd-x64": "0.17.19", 750 | "@esbuild/sunos-x64": "0.17.19", 751 | "@esbuild/win32-arm64": "0.17.19", 752 | "@esbuild/win32-ia32": "0.17.19", 753 | "@esbuild/win32-x64": "0.17.19" 754 | } 755 | }, 756 | "node_modules/escape-string-regexp": { 757 | "version": "4.0.0", 758 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 759 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 760 | "dev": true, 761 | "license": "MIT", 762 | "engines": { 763 | "node": ">=10" 764 | }, 765 | "funding": { 766 | "url": "https://github.com/sponsors/sindresorhus" 767 | } 768 | }, 769 | "node_modules/estree-walker": { 770 | "version": "0.6.1", 771 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", 772 | "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", 773 | "dev": true, 774 | "license": "MIT" 775 | }, 776 | "node_modules/exit-hook": { 777 | "version": "2.2.1", 778 | "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", 779 | "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==", 780 | "dev": true, 781 | "license": "MIT", 782 | "engines": { 783 | "node": ">=6" 784 | }, 785 | "funding": { 786 | "url": "https://github.com/sponsors/sindresorhus" 787 | } 788 | }, 789 | "node_modules/fsevents": { 790 | "version": "2.3.3", 791 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 792 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 793 | "dev": true, 794 | "hasInstallScript": true, 795 | "license": "MIT", 796 | "optional": true, 797 | "os": [ 798 | "darwin" 799 | ], 800 | "engines": { 801 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 802 | } 803 | }, 804 | "node_modules/function-bind": { 805 | "version": "1.1.2", 806 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 807 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 808 | "dev": true, 809 | "license": "MIT", 810 | "funding": { 811 | "url": "https://github.com/sponsors/ljharb" 812 | } 813 | }, 814 | "node_modules/get-source": { 815 | "version": "2.0.12", 816 | "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", 817 | "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", 818 | "dev": true, 819 | "license": "Unlicense", 820 | "dependencies": { 821 | "data-uri-to-buffer": "^2.0.0", 822 | "source-map": "^0.6.1" 823 | } 824 | }, 825 | "node_modules/glob-to-regexp": { 826 | "version": "0.4.1", 827 | "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", 828 | "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", 829 | "dev": true, 830 | "license": "BSD-2-Clause" 831 | }, 832 | "node_modules/hasown": { 833 | "version": "2.0.2", 834 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 835 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 836 | "dev": true, 837 | "license": "MIT", 838 | "dependencies": { 839 | "function-bind": "^1.1.2" 840 | }, 841 | "engines": { 842 | "node": ">= 0.4" 843 | } 844 | }, 845 | "node_modules/is-core-module": { 846 | "version": "2.15.1", 847 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", 848 | "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", 849 | "dev": true, 850 | "license": "MIT", 851 | "dependencies": { 852 | "hasown": "^2.0.2" 853 | }, 854 | "engines": { 855 | "node": ">= 0.4" 856 | }, 857 | "funding": { 858 | "url": "https://github.com/sponsors/ljharb" 859 | } 860 | }, 861 | "node_modules/itty-time": { 862 | "version": "1.0.6", 863 | "resolved": "https://registry.npmjs.org/itty-time/-/itty-time-1.0.6.tgz", 864 | "integrity": "sha512-+P8IZaLLBtFv8hCkIjcymZOp4UJ+xW6bSlQsXGqrkmJh7vSiMFSlNne0mCYagEE0N7HDNR5jJBRxwN0oYv61Rw==", 865 | "dev": true, 866 | "license": "MIT" 867 | }, 868 | "node_modules/magic-string": { 869 | "version": "0.25.9", 870 | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", 871 | "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", 872 | "dev": true, 873 | "license": "MIT", 874 | "dependencies": { 875 | "sourcemap-codec": "^1.4.8" 876 | } 877 | }, 878 | "node_modules/mime": { 879 | "version": "3.0.0", 880 | "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", 881 | "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", 882 | "dev": true, 883 | "license": "MIT", 884 | "bin": { 885 | "mime": "cli.js" 886 | }, 887 | "engines": { 888 | "node": ">=10.0.0" 889 | } 890 | }, 891 | "node_modules/miniflare": { 892 | "version": "3.20241205.0", 893 | "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20241205.0.tgz", 894 | "integrity": "sha512-Z0cTtIf6ZrcAJ3SrOI9EUM3s4dkGhNeU6Ubl8sroYhsPVD+rtz3m5+p6McHFWCkcMff1o60X5XEKVTmkz0gbpA==", 895 | "dev": true, 896 | "license": "MIT", 897 | "dependencies": { 898 | "@cspotcode/source-map-support": "0.8.1", 899 | "acorn": "^8.8.0", 900 | "acorn-walk": "^8.2.0", 901 | "capnp-ts": "^0.7.0", 902 | "exit-hook": "^2.2.1", 903 | "glob-to-regexp": "^0.4.1", 904 | "stoppable": "^1.1.0", 905 | "undici": "^5.28.4", 906 | "workerd": "1.20241205.0", 907 | "ws": "^8.18.0", 908 | "youch": "^3.2.2", 909 | "zod": "^3.22.3" 910 | }, 911 | "bin": { 912 | "miniflare": "bootstrap.js" 913 | }, 914 | "engines": { 915 | "node": ">=16.13" 916 | } 917 | }, 918 | "node_modules/ms": { 919 | "version": "2.1.3", 920 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 921 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 922 | "dev": true, 923 | "license": "MIT" 924 | }, 925 | "node_modules/mustache": { 926 | "version": "4.2.0", 927 | "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", 928 | "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", 929 | "dev": true, 930 | "license": "MIT", 931 | "bin": { 932 | "mustache": "bin/mustache" 933 | } 934 | }, 935 | "node_modules/nanoid": { 936 | "version": "3.3.8", 937 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", 938 | "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", 939 | "dev": true, 940 | "funding": [ 941 | { 942 | "type": "github", 943 | "url": "https://github.com/sponsors/ai" 944 | } 945 | ], 946 | "license": "MIT", 947 | "bin": { 948 | "nanoid": "bin/nanoid.cjs" 949 | }, 950 | "engines": { 951 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 952 | } 953 | }, 954 | "node_modules/node-forge": { 955 | "version": "1.3.1", 956 | "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", 957 | "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", 958 | "dev": true, 959 | "license": "(BSD-3-Clause OR GPL-2.0)", 960 | "engines": { 961 | "node": ">= 6.13.0" 962 | } 963 | }, 964 | "node_modules/ohash": { 965 | "version": "1.1.4", 966 | "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.4.tgz", 967 | "integrity": "sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==", 968 | "dev": true, 969 | "license": "MIT" 970 | }, 971 | "node_modules/path-parse": { 972 | "version": "1.0.7", 973 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 974 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", 975 | "dev": true, 976 | "license": "MIT" 977 | }, 978 | "node_modules/path-to-regexp": { 979 | "version": "6.3.0", 980 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", 981 | "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", 982 | "dev": true, 983 | "license": "MIT" 984 | }, 985 | "node_modules/pathe": { 986 | "version": "1.1.2", 987 | "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", 988 | "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", 989 | "dev": true, 990 | "license": "MIT" 991 | }, 992 | "node_modules/printable-characters": { 993 | "version": "1.0.42", 994 | "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", 995 | "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", 996 | "dev": true, 997 | "license": "Unlicense" 998 | }, 999 | "node_modules/readdirp": { 1000 | "version": "4.0.2", 1001 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", 1002 | "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", 1003 | "dev": true, 1004 | "license": "MIT", 1005 | "engines": { 1006 | "node": ">= 14.16.0" 1007 | }, 1008 | "funding": { 1009 | "type": "individual", 1010 | "url": "https://paulmillr.com/funding/" 1011 | } 1012 | }, 1013 | "node_modules/resolve": { 1014 | "version": "1.22.8", 1015 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", 1016 | "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", 1017 | "dev": true, 1018 | "license": "MIT", 1019 | "dependencies": { 1020 | "is-core-module": "^2.13.0", 1021 | "path-parse": "^1.0.7", 1022 | "supports-preserve-symlinks-flag": "^1.0.0" 1023 | }, 1024 | "bin": { 1025 | "resolve": "bin/resolve" 1026 | }, 1027 | "funding": { 1028 | "url": "https://github.com/sponsors/ljharb" 1029 | } 1030 | }, 1031 | "node_modules/rollup-plugin-inject": { 1032 | "version": "3.0.2", 1033 | "resolved": "https://registry.npmjs.org/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz", 1034 | "integrity": "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==", 1035 | "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject.", 1036 | "dev": true, 1037 | "license": "MIT", 1038 | "dependencies": { 1039 | "estree-walker": "^0.6.1", 1040 | "magic-string": "^0.25.3", 1041 | "rollup-pluginutils": "^2.8.1" 1042 | } 1043 | }, 1044 | "node_modules/rollup-plugin-node-polyfills": { 1045 | "version": "0.2.1", 1046 | "resolved": "https://registry.npmjs.org/rollup-plugin-node-polyfills/-/rollup-plugin-node-polyfills-0.2.1.tgz", 1047 | "integrity": "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==", 1048 | "dev": true, 1049 | "license": "MIT", 1050 | "dependencies": { 1051 | "rollup-plugin-inject": "^3.0.0" 1052 | } 1053 | }, 1054 | "node_modules/rollup-pluginutils": { 1055 | "version": "2.8.2", 1056 | "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", 1057 | "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", 1058 | "dev": true, 1059 | "license": "MIT", 1060 | "dependencies": { 1061 | "estree-walker": "^0.6.1" 1062 | } 1063 | }, 1064 | "node_modules/selfsigned": { 1065 | "version": "2.4.1", 1066 | "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", 1067 | "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", 1068 | "dev": true, 1069 | "license": "MIT", 1070 | "dependencies": { 1071 | "@types/node-forge": "^1.3.0", 1072 | "node-forge": "^1" 1073 | }, 1074 | "engines": { 1075 | "node": ">=10" 1076 | } 1077 | }, 1078 | "node_modules/source-map": { 1079 | "version": "0.6.1", 1080 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 1081 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 1082 | "dev": true, 1083 | "license": "BSD-3-Clause", 1084 | "engines": { 1085 | "node": ">=0.10.0" 1086 | } 1087 | }, 1088 | "node_modules/sourcemap-codec": { 1089 | "version": "1.4.8", 1090 | "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", 1091 | "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", 1092 | "deprecated": "Please use @jridgewell/sourcemap-codec instead", 1093 | "dev": true, 1094 | "license": "MIT" 1095 | }, 1096 | "node_modules/stacktracey": { 1097 | "version": "2.1.8", 1098 | "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", 1099 | "integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==", 1100 | "dev": true, 1101 | "license": "Unlicense", 1102 | "dependencies": { 1103 | "as-table": "^1.0.36", 1104 | "get-source": "^2.0.12" 1105 | } 1106 | }, 1107 | "node_modules/stoppable": { 1108 | "version": "1.1.0", 1109 | "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", 1110 | "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", 1111 | "dev": true, 1112 | "license": "MIT", 1113 | "engines": { 1114 | "node": ">=4", 1115 | "npm": ">=6" 1116 | } 1117 | }, 1118 | "node_modules/supports-preserve-symlinks-flag": { 1119 | "version": "1.0.0", 1120 | "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", 1121 | "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", 1122 | "dev": true, 1123 | "license": "MIT", 1124 | "engines": { 1125 | "node": ">= 0.4" 1126 | }, 1127 | "funding": { 1128 | "url": "https://github.com/sponsors/ljharb" 1129 | } 1130 | }, 1131 | "node_modules/tslib": { 1132 | "version": "2.8.1", 1133 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", 1134 | "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", 1135 | "dev": true, 1136 | "license": "0BSD" 1137 | }, 1138 | "node_modules/ufo": { 1139 | "version": "1.5.4", 1140 | "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", 1141 | "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==", 1142 | "dev": true, 1143 | "license": "MIT" 1144 | }, 1145 | "node_modules/undici": { 1146 | "version": "5.28.4", 1147 | "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", 1148 | "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", 1149 | "dev": true, 1150 | "license": "MIT", 1151 | "dependencies": { 1152 | "@fastify/busboy": "^2.0.0" 1153 | }, 1154 | "engines": { 1155 | "node": ">=14.0" 1156 | } 1157 | }, 1158 | "node_modules/undici-types": { 1159 | "version": "6.20.0", 1160 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", 1161 | "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", 1162 | "dev": true, 1163 | "license": "MIT" 1164 | }, 1165 | "node_modules/unenv": { 1166 | "name": "unenv-nightly", 1167 | "version": "2.0.0-20241204-140205-a5d5190", 1168 | "resolved": "https://registry.npmjs.org/unenv-nightly/-/unenv-nightly-2.0.0-20241204-140205-a5d5190.tgz", 1169 | "integrity": "sha512-jpmAytLeiiW01pl5bhVn9wYJ4vtiLdhGe10oXlJBuQEX8mxjxO8BlEXGHU4vr4yEikjFP1wsomTHt/CLU8kUwg==", 1170 | "dev": true, 1171 | "license": "MIT", 1172 | "dependencies": { 1173 | "defu": "^6.1.4", 1174 | "ohash": "^1.1.4", 1175 | "pathe": "^1.1.2", 1176 | "ufo": "^1.5.4" 1177 | } 1178 | }, 1179 | "node_modules/workerd": { 1180 | "version": "1.20241205.0", 1181 | "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20241205.0.tgz", 1182 | "integrity": "sha512-vso/2n0c5SdBDWiD+Sx5gM7unA6SiZXRVUHDqH1euoP/9mFVHZF8icoYsNLB87b/TX8zNgpae+I5N/xFpd9v0g==", 1183 | "dev": true, 1184 | "hasInstallScript": true, 1185 | "license": "Apache-2.0", 1186 | "bin": { 1187 | "workerd": "bin/workerd" 1188 | }, 1189 | "engines": { 1190 | "node": ">=16" 1191 | }, 1192 | "optionalDependencies": { 1193 | "@cloudflare/workerd-darwin-64": "1.20241205.0", 1194 | "@cloudflare/workerd-darwin-arm64": "1.20241205.0", 1195 | "@cloudflare/workerd-linux-64": "1.20241205.0", 1196 | "@cloudflare/workerd-linux-arm64": "1.20241205.0", 1197 | "@cloudflare/workerd-windows-64": "1.20241205.0" 1198 | } 1199 | }, 1200 | "node_modules/wrangler": { 1201 | "version": "3.95.0", 1202 | "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.95.0.tgz", 1203 | "integrity": "sha512-3w5852i3FNyDz421K2Qk4v5L8jjwegO5O8E1+VAQmjnm82HFNxpIRUBq0bmM7CTLvOPI/Jjcmj/eAWjQBL7QYg==", 1204 | "dev": true, 1205 | "license": "MIT OR Apache-2.0", 1206 | "dependencies": { 1207 | "@cloudflare/kv-asset-handler": "0.3.4", 1208 | "@cloudflare/workers-shared": "0.11.0", 1209 | "@esbuild-plugins/node-globals-polyfill": "^0.2.3", 1210 | "@esbuild-plugins/node-modules-polyfill": "^0.2.2", 1211 | "blake3-wasm": "^2.1.5", 1212 | "chokidar": "^4.0.1", 1213 | "date-fns": "^4.1.0", 1214 | "esbuild": "0.17.19", 1215 | "itty-time": "^1.0.6", 1216 | "miniflare": "3.20241205.0", 1217 | "nanoid": "^3.3.3", 1218 | "path-to-regexp": "^6.3.0", 1219 | "resolve": "^1.22.8", 1220 | "selfsigned": "^2.0.1", 1221 | "source-map": "^0.6.1", 1222 | "unenv": "npm:unenv-nightly@2.0.0-20241204-140205-a5d5190", 1223 | "workerd": "1.20241205.0", 1224 | "xxhash-wasm": "^1.0.1" 1225 | }, 1226 | "bin": { 1227 | "wrangler": "bin/wrangler.js", 1228 | "wrangler2": "bin/wrangler.js" 1229 | }, 1230 | "engines": { 1231 | "node": ">=16.17.0" 1232 | }, 1233 | "optionalDependencies": { 1234 | "fsevents": "~2.3.2" 1235 | }, 1236 | "peerDependencies": { 1237 | "@cloudflare/workers-types": "^4.20241205.0" 1238 | }, 1239 | "peerDependenciesMeta": { 1240 | "@cloudflare/workers-types": { 1241 | "optional": true 1242 | } 1243 | } 1244 | }, 1245 | "node_modules/ws": { 1246 | "version": "8.18.0", 1247 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", 1248 | "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", 1249 | "dev": true, 1250 | "license": "MIT", 1251 | "engines": { 1252 | "node": ">=10.0.0" 1253 | }, 1254 | "peerDependencies": { 1255 | "bufferutil": "^4.0.1", 1256 | "utf-8-validate": ">=5.0.2" 1257 | }, 1258 | "peerDependenciesMeta": { 1259 | "bufferutil": { 1260 | "optional": true 1261 | }, 1262 | "utf-8-validate": { 1263 | "optional": true 1264 | } 1265 | } 1266 | }, 1267 | "node_modules/xxhash-wasm": { 1268 | "version": "1.1.0", 1269 | "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", 1270 | "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==", 1271 | "dev": true, 1272 | "license": "MIT" 1273 | }, 1274 | "node_modules/youch": { 1275 | "version": "3.3.4", 1276 | "resolved": "https://registry.npmjs.org/youch/-/youch-3.3.4.tgz", 1277 | "integrity": "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==", 1278 | "dev": true, 1279 | "license": "MIT", 1280 | "dependencies": { 1281 | "cookie": "^0.7.1", 1282 | "mustache": "^4.2.0", 1283 | "stacktracey": "^2.1.8" 1284 | } 1285 | }, 1286 | "node_modules/zod": { 1287 | "version": "3.24.1", 1288 | "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", 1289 | "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", 1290 | "dev": true, 1291 | "license": "MIT", 1292 | "funding": { 1293 | "url": "https://github.com/sponsors/colinhacks" 1294 | } 1295 | } 1296 | } 1297 | } 1298 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stremio-gdrive", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "deploy": "wrangler deploy", 7 | "dev": "wrangler dev", 8 | "start": "wrangler dev" 9 | }, 10 | "devDependencies": { 11 | "wrangler": "^3.60.3" 12 | } 13 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const CREDENTIALS = { 2 | clientId: "", 3 | clientSecret: "", 4 | refreshToken: "", 5 | }; 6 | 7 | const CONFIG = { 8 | resolutions: ["2160p", "1080p", "720p", "480p", "Unknown"], 9 | qualities: [ 10 | "BluRay REMUX", 11 | "BluRay", 12 | "WEB-DL", 13 | "WEBRip", 14 | "HDRip", 15 | "HC HD-Rip", 16 | "DVDRip", 17 | "HDTV", 18 | "CAM", 19 | "TS", 20 | "TC", 21 | "SCR", 22 | "Unknown", 23 | ], 24 | visualTags: ["HDR10+", "HDR10", "HDR", "DV", "IMAX", "AI"], 25 | sortBy: ["resolution", "visualTag", "size", "quality"], 26 | showAudioFiles: false, 27 | considerHdrTagsAsEqual: true, 28 | addonName: "GDrive", 29 | prioritiseLanguage: null, 30 | proxiedPlayback: true, 31 | strictTitleCheck: false, 32 | tmdbApiKey: null, 33 | enableSearchCatalog: true, 34 | enableVideoCatalog: true, 35 | maxFilesToFetch: 1000, 36 | driveQueryTerms: { 37 | episodeFormat: "fullText", 38 | titleName: "name", 39 | }, 40 | }; 41 | 42 | const MANIFEST = { 43 | id: "stremio.gdrive.worker", 44 | version: "1.0.0", 45 | name: CONFIG.addonName, 46 | description: "Stream your files from Google Drive within Stremio!", 47 | catalogs: [], 48 | resources: [ 49 | { 50 | name: "stream", 51 | types: ["movie", "series", "anime"], 52 | }, 53 | ], 54 | types: ["movie", "series"], 55 | }; 56 | 57 | const HEADERS = { 58 | "Content-Type": "application/json", 59 | "Access-Control-Allow-Origin": "*", 60 | "Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS", 61 | "Access-Control-Max-Age": "86400", 62 | }; 63 | 64 | const API_ENDPOINTS = { 65 | DRIVE_FETCH_FILES: "https://content.googleapis.com/drive/v3/files", 66 | DRIVE_FETCH_FILE: "https://content.googleapis.com/drive/v3/files/{fileId}", 67 | DRIVE_STREAM_FILE: 68 | "https://www.googleapis.com/drive/v3/files/{fileId}?alt=media&file_name={filename}", 69 | DRIVE_TOKEN: "https://oauth2.googleapis.com/token", 70 | CINEMETA: "https://v3-cinemeta.strem.io/meta/{type}/{id}.json", 71 | IMDB_SUGGEST: "https://v3.sg.media-imdb.com/suggestion/a/{id}.json", 72 | TMDB_FIND: 73 | "https://api.themoviedb.org/3/find/{id}?api_key={apiKey}&external_source=imdb_id", 74 | TMDB_DETAILS: "https://api.themoviedb.org/3/{type}/{id}?api_key={apiKey}", 75 | }; 76 | 77 | const REGEX_PATTERNS = { 78 | validStreamRequest: /\/stream\/(movie|series)\/([a-zA-Z0-9%:\-_]+)\.json/, 79 | validPlaybackRequest: /\/playback\/([a-zA-Z0-9%:\-_]+)\/(.+)/, 80 | validCatalogRequest: 81 | /\/catalog\/movie\/([a-zA-Z0-9%:\-_]+)(\/search=(.+))?\.json/, 82 | validMetaRequest: /\/meta\/(movie)\/([a-zA-Z0-9%:\-_]+)\.json/, 83 | resolutions: { 84 | "2160p": /(? 247 | CONFIG.considerHdrTagsAsEqual && tag.startsWith("HDR") 248 | ? CONFIG.visualTags.indexOf("HDR10+") 249 | : CONFIG.visualTags.indexOf(tag); 250 | const aVisualTagIndex = a.visualTags.reduce( 251 | (minIndex, tag) => Math.min(minIndex, getIndexOfTag(tag)), 252 | CONFIG.visualTags.length 253 | ); 254 | 255 | const bVisualTagIndex = b.visualTags.reduce( 256 | (minIndex, tag) => Math.min(minIndex, getIndexOfTag(tag)), 257 | CONFIG.visualTags.length 258 | ); 259 | // Sort by the visual tag index 260 | return aVisualTagIndex - bVisualTagIndex; 261 | } else if (field === "durationAsc") { 262 | return a.duration - b.duration; 263 | } else if (field === "durationDesc") { 264 | return b.duration - a.duration; 265 | } 266 | return 0; 267 | } 268 | 269 | function createStream(parsedFile, accessToken) { 270 | let name = parsedFile.type.startsWith("audio") 271 | ? `[🎵 Audio] ${MANIFEST.name} ${parsedFile.extension.toUpperCase()}` 272 | : `${MANIFEST.name} ${parsedFile.resolution}`; 273 | 274 | let description = `🎥 ${parsedFile.quality} ${ 275 | parsedFile.encode ? "🎞️ " + parsedFile.encode : "" 276 | }`; 277 | 278 | if (parsedFile.visualTags.length > 0 || parsedFile.audioTags.length > 0) { 279 | description += "\n"; 280 | 281 | description += 282 | parsedFile.visualTags.length > 0 283 | ? `📺 ${parsedFile.visualTags.join(" | ")} ` 284 | : ""; 285 | description += 286 | parsedFile.audioTags.length > 0 287 | ? `🎧 ${parsedFile.audioTags.join(" | ")}` 288 | : ""; 289 | } 290 | 291 | description += `\n📦 ${parsedFile.formattedSize}`; 292 | if (parsedFile.languages.length !== 0) { 293 | description += `\n🔊 ${parsedFile.languages.join(" | ")}`; 294 | } 295 | 296 | description += `\n📄 ${parsedFile.name}`; 297 | 298 | if (parsedFile.duration) { 299 | description += `\n⏱️ ${formatDuration(parsedFile.duration)}`; 300 | } 301 | const combinedTags = [ 302 | parsedFile.resolution, 303 | parsedFile.quality, 304 | parsedFile.encode, 305 | ...parsedFile.visualTags, 306 | ...parsedFile.audioTags, 307 | ...parsedFile.languages, 308 | ]; 309 | 310 | const stream = { 311 | name: name, 312 | description: description, 313 | url: "", 314 | behaviorHints: { 315 | videoSize: parseInt(parsedFile.size) || 0, 316 | filename: parsedFile.name, 317 | bingeGroup: `${MANIFEST.name}|${combinedTags.join("|")}`, 318 | }, 319 | }; 320 | 321 | if (CONFIG.proxiedPlayback) { 322 | stream.url = `${globalThis.playbackUrl}/${ 323 | parsedFile.id 324 | }/${encodeURIComponent(parsedFile.name)}`; 325 | } else { 326 | stream.url = API_ENDPOINTS.DRIVE_STREAM_FILE.replace( 327 | "{fileId}", 328 | parsedFile.id 329 | ).replace("{filename}", parsedFile.name); 330 | stream.behaviorHints.proxyHeaders = { 331 | request: { 332 | Accept: "application/json", 333 | Authorization: `Bearer ${accessToken}`, 334 | }, 335 | }; 336 | stream.behaviorHints.notWebReady = true; 337 | } 338 | 339 | return stream; 340 | } 341 | 342 | function createErrorStream(description) { 343 | return { 344 | name: `[⚠️] ${MANIFEST.name}`, 345 | description: description, 346 | externalUrl: "https://github.com/Viren070/stremio-gdrive-addon", 347 | }; 348 | } 349 | 350 | function sortParsedFiles(parsedFiles) { 351 | parsedFiles.sort((a, b) => { 352 | const languageComparison = compareLanguages(a, b); 353 | if (languageComparison !== 0) return languageComparison; 354 | 355 | for (const sortByField of CONFIG.sortBy) { 356 | const fieldComparison = compareByField(a, b, sortByField); 357 | if (fieldComparison !== 0) return fieldComparison; 358 | } 359 | 360 | // move audio files to the end 361 | if (a.type.startsWith("audio") && !b.type.startsWith("audio")) return 1; 362 | if (!a.type.startsWith("audio") && b.type.startsWith("audio")) 363 | return -1; 364 | 365 | return 0; 366 | }); 367 | } 368 | 369 | function parseAndFilterFiles(files) { 370 | return files 371 | .map((file) => parseFile(file)) 372 | .filter( 373 | (parsedFile) => 374 | CONFIG.resolutions.includes(parsedFile.resolution) && 375 | CONFIG.qualities.includes(parsedFile.quality) && 376 | parsedFile.visualTags.every((tag) => 377 | CONFIG.visualTags.includes(tag) 378 | ) 379 | ); 380 | } 381 | 382 | function parseFile(file) { 383 | let resolution = 384 | Object.entries(REGEX_PATTERNS.resolutions).find(([_, pattern]) => 385 | pattern.test(file.name) 386 | )?.[0] || "Unknown"; 387 | let quality = 388 | Object.entries(REGEX_PATTERNS.qualities).find(([_, pattern]) => 389 | pattern.test(file.name) 390 | )?.[0] || "Unknown"; 391 | let visualTags = Object.entries(REGEX_PATTERNS.visualTags) 392 | .filter(([_, pattern]) => pattern.test(file.name)) 393 | .map(([tag]) => tag); 394 | let audioTags = Object.entries(REGEX_PATTERNS.audioTags) 395 | .filter(([_, pattern]) => pattern.test(file.name)) 396 | .map(([tag]) => tag); 397 | let encode = 398 | Object.entries(REGEX_PATTERNS.encodes).find(([_, pattern]) => 399 | pattern.test(file.name) 400 | )?.[0] || ""; 401 | let languages = Object.entries(REGEX_PATTERNS.languages) 402 | .filter(([_, pattern]) => pattern.test(file.name)) 403 | .map(([tag]) => tag); 404 | 405 | if (visualTags.includes("HDR10+")) { 406 | visualTags = visualTags.filter( 407 | (tag) => tag !== "HDR" && tag !== "HDR10" 408 | ); 409 | } else if (visualTags.includes("HDR10")) { 410 | visualTags = visualTags.filter((tag) => tag !== "HDR"); 411 | } 412 | 413 | return { 414 | id: file.id, 415 | name: file.name.trim(), 416 | size: file.size, 417 | formattedSize: parseInt(file.size) 418 | ? formatSize(parseInt(file.size)) 419 | : "Unknown", 420 | resolution: resolution, 421 | quality: quality, 422 | languages: languages, 423 | encode: encode, 424 | audioTags: audioTags, 425 | visualTags: visualTags, 426 | duration: 427 | parseInt(file.videoMediaMetadata?.durationMillis) || undefined, 428 | type: file.mimeType, 429 | extension: file.fileExtension, 430 | }; 431 | } 432 | 433 | function isConfigValid() { 434 | const requiredFields = [ 435 | { 436 | value: CREDENTIALS.clientId, 437 | error: "Missing clientId. Add your client ID to the credentials object", 438 | }, 439 | { 440 | value: CREDENTIALS.clientSecret, 441 | error: "Missing clientSecret! Add your client secret to the credentials object", 442 | }, 443 | { 444 | value: CREDENTIALS.refreshToken, 445 | error: "Missing refreshToken! Add your refresh token to the credentials object", 446 | }, 447 | { 448 | value: CONFIG.addonName, 449 | error: "Missing addonName! Provide it in the config object", 450 | }, 451 | ]; 452 | 453 | for (const { value, error } of requiredFields) { 454 | if (!value) { 455 | console.error({ message: error, yourValue: value }); 456 | return false; 457 | } 458 | } 459 | 460 | const validValues = { 461 | resolutions: [...Object.keys(REGEX_PATTERNS.resolutions), "Unknown"], 462 | qualities: [...Object.keys(REGEX_PATTERNS.qualities), "Unknown"], 463 | sortBy: [ 464 | "resolution", 465 | "size", 466 | "quality", 467 | "visualTag", 468 | "durationAsc", 469 | "durationDesc", 470 | ], 471 | languages: [...Object.keys(REGEX_PATTERNS.languages), "Unknown"], 472 | visualTags: [...Object.keys(REGEX_PATTERNS.visualTags)], 473 | }; 474 | 475 | const keyToSingular = { 476 | resolutions: "resolution", 477 | qualities: "quality", 478 | sortBy: "sort criterion", 479 | visualTags: "visual tag", 480 | }; 481 | 482 | for (const key of ["resolutions", "qualities", "sortBy", "visualTags"]) { 483 | const configValue = CONFIG[key]; 484 | if (!Array.isArray(configValue)) { 485 | console.error(`Invalid ${key}: ${configValue} is not an array`); 486 | return false; 487 | } 488 | for (const value of CONFIG[key]) { 489 | if (!validValues[key].includes(value)) { 490 | console.error({ 491 | message: `Invalid ${keyToSingular[key]}: ${value}`, 492 | validValues: validValues[key], 493 | }); 494 | return false; 495 | } 496 | } 497 | } 498 | 499 | if ( 500 | CONFIG.prioritiseLanguage && 501 | !validValues.languages.includes(CONFIG.prioritiseLanguage) 502 | ) { 503 | console.error({ 504 | message: `Invalid prioritised language: ${CONFIG.prioritiseLanguage}`, 505 | validValues: validValues.languages, 506 | }); 507 | return false; 508 | } 509 | 510 | return true; 511 | } 512 | 513 | async function getMetadata(type, fullId) { 514 | let id = fullId; 515 | 516 | if (id.startsWith("kitsu")) { 517 | id = id.split(":")[0] + ":" + id.split(":")[1]; // Remove the :1 at the end 518 | const meta = await getKitsuMeta(type, id); 519 | if (meta) { 520 | console.log({ 521 | message: "Successfully retrieved metadata from Kitsu", 522 | meta, 523 | }); 524 | return meta; 525 | } 526 | 527 | console.error({ 528 | message: "Failed to get metadata from Kitsu, returning null", 529 | }); 530 | 531 | return null; 532 | } 533 | 534 | 535 | if (CONFIG.tmdbApiKey) { 536 | try { 537 | const meta = await getTmdbMeta(type, fullId); 538 | if (meta) { 539 | console.log({ 540 | message: "Successfully retrieved metadata from TMDb", 541 | meta, 542 | }); 543 | return meta; 544 | } 545 | } catch (error) { 546 | console.error({ 547 | message: "Error fetching metadata from TMDb", 548 | error: error.toString(), 549 | }); 550 | } 551 | } 552 | try { 553 | const meta = await getCinemetaMeta(type, id); 554 | if (meta) { 555 | console.log({ 556 | message: "Successfully retrieved metadata from Cinemeta", 557 | meta, 558 | }); 559 | return meta; 560 | } 561 | } catch (error) { 562 | console.error({ 563 | message: "Error fetching metadata from Cinemeta", 564 | error: error.toString(), 565 | }); 566 | } 567 | 568 | try { 569 | const meta = await getImdbSuggestionMeta(id); 570 | if (meta) { 571 | console.log({ 572 | message: 573 | "Successfully retrieved metadata from IMDb Suggestions", 574 | meta, 575 | }); 576 | return meta; 577 | } 578 | } catch (error) { 579 | console.error({ 580 | message: "Error fetching metadata from IMDb Suggestions", 581 | error: error.toString(), 582 | }); 583 | } 584 | 585 | console.error({ 586 | message: 587 | "Failed to get metadata from Cinemeta or IMDb Suggestions, returning null", 588 | }); 589 | return null; 590 | } 591 | 592 | async function getKitsuMeta(type, id) { 593 | console.log({ message: "Fetching metadata from Kitsu", type, id }); 594 | const url = `https://anime-kitsu.strem.fun/meta/${type}/${id}.json`; 595 | console.log({ url }); 596 | const response = await fetch(url); 597 | if (!response.ok) { 598 | let err = await response.text(); 599 | throw new Error(err); 600 | } 601 | const data = await response.json(); 602 | if (!data?.meta) { 603 | throw new Error("Meta object not found in response"); 604 | } 605 | if (!data.meta.name || !data.meta.year) { 606 | throw new Error("Either name or year not found in meta object"); 607 | } 608 | return { 609 | name: data.meta.name, 610 | year: data.meta.year, 611 | }; 612 | } 613 | 614 | async function getTmdbMeta(type, id) { 615 | let response; 616 | let result; 617 | if (id.startsWith("tmdb")) { 618 | if (!CONFIG.tmdbApiKey) { 619 | throw new Error("TMDB ID detected but no API key provided"); 620 | } 621 | const url = API_ENDPOINTS.TMDB_DETAILS.replace("{type}", type === "movie" ? "movie" : "tv") 622 | .replace("{id}", id.split(":")[1]) 623 | .replace("{apiKey}", CONFIG.tmdbApiKey); 624 | console.log({ 625 | message: "Fetching data from TMDB with TMDB ID", 626 | url, 627 | }); 628 | response = await fetch(url); 629 | 630 | if (!response.ok) { 631 | let err = await response.text(); 632 | throw new Error(`${response.status} - ${response.statusText}: ${err}`); 633 | } 634 | 635 | result = await response.json(); 636 | 637 | } else { 638 | const url = API_ENDPOINTS.TMDB_FIND.replace("{id}", id.split(":")[0]) 639 | .replace("{apiKey}", CONFIG.tmdbApiKey) 640 | console.log({ 641 | message: "Fetching data from TMDB with external source", 642 | url, 643 | }) 644 | response = await fetch(url); 645 | 646 | if (!response.ok) { 647 | let err = await response.text(); 648 | throw new Error(`${response.status} - ${response.statusText}: ${err}`); 649 | } 650 | const data = await response.json(); 651 | if (!data?.movie_results && !data?.tv_results) { 652 | throw new Error("No results found in response"); 653 | } 654 | 655 | result = data.movie_results[0] || data.tv_results[0]; 656 | 657 | } 658 | 659 | if (!result) { 660 | throw new Error("No results found in response"); 661 | } 662 | console.log({ message: "Got data from TMDB", result }); 663 | 664 | if ( 665 | (!result.name && !result.title) || 666 | (!result.release_date && !result.first_air_date) 667 | ) { 668 | throw new Error("Either title or release date not found in result"); 669 | } 670 | 671 | return { 672 | name: result.name || result.title, 673 | year: (result.release_date || result.first_air_date).split("-")[0], 674 | }; 675 | } 676 | 677 | async function getCinemetaMeta(type, id) { 678 | id = id.split(":")[0]; 679 | const response = await fetch( 680 | API_ENDPOINTS.CINEMETA.replace("{type}", type).replace("{id}", id) 681 | ); 682 | if (!response.ok) { 683 | let err = await response.text(); 684 | throw new Error(err); 685 | } 686 | const data = await response.json(); 687 | if (!data?.meta) { 688 | throw new Error("Meta object not found in response"); 689 | } 690 | if (!data.meta.name || !data.meta.year) { 691 | throw new Error("Either name or year not found in meta object"); 692 | } 693 | return { 694 | name: data.meta.name, 695 | year: data.meta.year, 696 | }; 697 | } 698 | 699 | async function getImdbSuggestionMeta(id) { 700 | id = id.split(":")[0]; 701 | const response = await fetch( 702 | API_ENDPOINTS.IMDB_SUGGEST.replace("{id}", id) 703 | ); 704 | if (!response.ok) { 705 | let err = await response.text(); 706 | throw new Error(err); 707 | } 708 | const data = await response.json(); 709 | if (!data?.d) { 710 | throw new Error("No suggestions in d object"); 711 | } 712 | 713 | const item = data.d.find((item) => item.id === id); 714 | if (!item) { 715 | throw new Error("No matching item found with the given id"); 716 | } 717 | 718 | if (!item?.l || !item?.y) { 719 | throw new Error("Missing name or year"); 720 | } 721 | 722 | return { 723 | name: item.l, 724 | year: item.y, 725 | }; 726 | } 727 | 728 | async function getAccessToken() { 729 | const params = new URLSearchParams({ 730 | client_id: CREDENTIALS.clientId, 731 | client_secret: CREDENTIALS.clientSecret, 732 | refresh_token: CREDENTIALS.refreshToken, 733 | grant_type: "refresh_token", 734 | }); 735 | 736 | try { 737 | const response = await fetch(API_ENDPOINTS.DRIVE_TOKEN, { 738 | method: "POST", 739 | body: params, 740 | headers: { "Content-Type": "application/x-www-form-urlencoded" }, 741 | }); 742 | 743 | if (!response.ok) { 744 | let err = await response.json(); 745 | throw new Error(JSON.stringify(err)); 746 | } 747 | 748 | const { access_token } = await response.json(); 749 | return access_token; 750 | } catch (error) { 751 | console.error({ 752 | message: "Failed to refresh token", 753 | error: JSON.parse(error.message), 754 | }); 755 | return undefined; 756 | } 757 | } 758 | 759 | async function fetchFiles(fetchUrl, accessToken) { 760 | try { 761 | const response = await fetch(fetchUrl.toString(), { 762 | headers: { Authorization: `Bearer ${accessToken}` }, 763 | }); 764 | 765 | if (!response.ok) { 766 | let err = await response.text(); 767 | throw new Error(err); 768 | } 769 | // handle paginated results 770 | 771 | const results = await response.json(); 772 | console.log({ 773 | message: "Initial search yielded results", 774 | numItems: results.files.length, 775 | }); 776 | while (results.nextPageToken) { 777 | fetchUrl.searchParams.set("pageToken", results.nextPageToken); 778 | const nextPageResponse = await fetch(fetchUrl.toString(), { 779 | headers: { Authorization: `Bearer ${accessToken}` }, 780 | }); 781 | 782 | if (!nextPageResponse.ok) { 783 | let err = await nextPageResponse.text(); 784 | throw new Error(err); 785 | } 786 | 787 | const nextPageResults = await nextPageResponse.json(); 788 | results.files = [...results.files, ...nextPageResults.files]; 789 | results.nextPageToken = nextPageResults.nextPageToken; 790 | console.log({ 791 | message: "Searched next page", 792 | nextPageResults: nextPageResults.files.length, 793 | nextPageToken: nextPageResults.nextPageToken, 794 | totalResults: results.files.length, 795 | }); 796 | if (results.files.length >= CONFIG.maxFilesToFetch) { 797 | console.log({ 798 | message: "Reached maximum number of files", 799 | files: results.files.length, 800 | }); 801 | break; 802 | } 803 | if (nextPageResults.files.length === 0) { 804 | console.log({ message: "No more files to fetch" }); 805 | break; 806 | } 807 | } 808 | 809 | return results; 810 | } catch (error) { 811 | console.error({ 812 | message: "Could not fetch files from Google Drive", 813 | error: error.toString(), 814 | }); 815 | return null; 816 | } 817 | } 818 | 819 | async function fetchFile(fileId, accessToken) { 820 | try { 821 | const fetchUrl = new URL( 822 | API_ENDPOINTS.DRIVE_FETCH_FILE.replace("{fileId}", fileId) 823 | ); 824 | const searchParams = { 825 | supportsAllDrives: true, 826 | fields: "id,name,mimeType,size,videoMediaMetadata,fileExtension,createdTime,thumbnailLink,iconLink", 827 | }; 828 | fetchUrl.search = new URLSearchParams(searchParams).toString(); 829 | const response = await fetch(fetchUrl.toString(), { 830 | headers: { Authorization: `Bearer ${accessToken}` }, 831 | }); 832 | 833 | if (!response.ok) { 834 | let err = await response.text(); 835 | throw new Error(err); 836 | } 837 | 838 | const file = await response.json(); 839 | return file; 840 | } catch (error) { 841 | console.error({ 842 | message: "Could not fetch file from Google Drive", 843 | error: error.toString(), 844 | }); 845 | return null; 846 | } 847 | } 848 | 849 | function buildBaseSearchQuery(query) { 850 | query = query.replace(/'/g, "\\'"); 851 | let q = `name contains '${query}' and trashed=false and not name contains 'trailer' and not name contains 'sample'`; 852 | 853 | if (CONFIG.showAudioFiles) { 854 | q += ` and (mimeType contains 'video/' or mimeType contains 'audio/')`; 855 | } else { 856 | q += ` and mimeType contains 'video/'`; 857 | } 858 | 859 | console.log({ message: "Built base search query", query: q }); 860 | return q; 861 | } 862 | 863 | async function buildSearchQuery(streamRequest) { 864 | const { name, year } = streamRequest.metadata; 865 | 866 | let query = 867 | "trashed=false and not name contains 'trailer' and not name contains 'sample'"; 868 | 869 | query += CONFIG.showAudioFiles 870 | ? ` and (mimeType contains 'video/' or mimeType contains 'audio/')` 871 | : ` and mimeType contains 'video/'`; 872 | 873 | const sanitisedName = name 874 | .replace(/[^\p{L}\p{N}\s]/gu, "") 875 | .replace(/'/g, "\\'"); 876 | const nameWithoutApostrophes = name.replace(/[^a-zA-Z0-9\s]/g, ""); 877 | 878 | if (streamRequest.type === "movie") 879 | query += ` and (${CONFIG.driveQueryTerms.titleName} contains '${sanitisedName} ${year}' or ${CONFIG.driveQueryTerms.titleName} contains '${nameWithoutApostrophes} ${year}')`; 880 | if (streamRequest.type === "series") 881 | query += ` and (${CONFIG.driveQueryTerms.titleName} contains '${sanitisedName}' or ${CONFIG.driveQueryTerms.titleName} contains '${nameWithoutApostrophes}')`; 882 | 883 | const season = streamRequest.season; 884 | const episode = streamRequest.episode; 885 | if (!season || !episode) return query; 886 | 887 | const formats = []; 888 | let zeroPaddedSeason = season.toString().padStart(2, "0"); 889 | let zeroPaddedEpisode = episode.toString().padStart(2, "0"); 890 | 891 | const getFormats = (season, episode) => { 892 | return [ 893 | [`s${season}e${episode}`], 894 | [`s${season}`, `e${episode}`], 895 | [`s${season}.e${episode}`], 896 | [`${season}x${episode}`], 897 | [`s${season}xe${episode}`], 898 | [`season ${season}`, `episode ${episode}`], 899 | [`s${season}`, `ep${episode}`], 900 | ]; 901 | }; 902 | 903 | formats.push(...getFormats(season, episode)); 904 | 905 | if (zeroPaddedSeason !== season.toString()) { 906 | formats.push(...getFormats(zeroPaddedSeason, episode)); 907 | } 908 | 909 | if (zeroPaddedEpisode !== episode.toString()) { 910 | formats.push(...getFormats(season, zeroPaddedEpisode)); 911 | } 912 | 913 | if ( 914 | zeroPaddedSeason !== season.toString() && 915 | zeroPaddedEpisode !== episode.toString() 916 | ) { 917 | formats.push(...getFormats(zeroPaddedSeason, zeroPaddedEpisode)); 918 | } 919 | 920 | query += ` and (${formats 921 | .map( 922 | (formatList) => 923 | `(${formatList 924 | .map( 925 | (format) => 926 | `${CONFIG.driveQueryTerms.episodeFormat} contains '${format}'` 927 | ) 928 | .join(" and ")})` 929 | ) 930 | .join(" or ")})`; 931 | 932 | return query; 933 | } 934 | 935 | async function handleRequest(request) { 936 | try { 937 | const url = new URL( 938 | decodeURIComponent(request.url).replace("%3A", ":") 939 | ); 940 | globalThis.playbackUrl = url.origin + "/playback"; 941 | 942 | if (url.pathname === "/manifest.json") { 943 | const manifest = MANIFEST; 944 | manifest.catalogs = []; 945 | manifest.resources = [ 946 | { name: "stream", types: ["movie", "series", "anime"] }, 947 | ]; 948 | if (CONFIG.enableSearchCatalog) { 949 | manifest.catalogs.push({ 950 | type: "movie", 951 | id: "gdrive_list", 952 | name: "Google Drive", 953 | }); 954 | } 955 | if (CONFIG.enableVideoCatalog) { 956 | manifest.catalogs.push({ 957 | type: "movie", 958 | id: "gdrive_search", 959 | name: "Google Drive Search", 960 | extra: [ 961 | { 962 | name: "search", 963 | isRequired: true, 964 | }, 965 | ], 966 | }); 967 | } 968 | if (CONFIG.enableVideoCatalog || CONFIG.enableSearchCatalog) { 969 | manifest.resources.push({ 970 | name: "catalog", 971 | types: ["movie"], 972 | }); 973 | manifest.resources.push({ 974 | name: "meta", 975 | types: ["movie", "series", "anime"], 976 | idPrefixes: ["gdrive:"], 977 | }); 978 | } 979 | return createJsonResponse(manifest); 980 | } 981 | 982 | if (url.pathname === "/") 983 | return Response.redirect(url.origin + "/manifest.json", 301); 984 | 985 | const streamMatch = REGEX_PATTERNS.validStreamRequest.exec( 986 | url.pathname 987 | ); 988 | const playbackMatch = REGEX_PATTERNS.validPlaybackRequest.exec( 989 | url.pathname 990 | ); 991 | const catalogMatch = REGEX_PATTERNS.validCatalogRequest.exec( 992 | url.pathname 993 | ); 994 | const metaMatch = REGEX_PATTERNS.validMetaRequest.exec(url.pathname); 995 | 996 | if (!(playbackMatch || streamMatch || catalogMatch || metaMatch)) 997 | return new Response("Bad Request", { status: 400 }); 998 | 999 | if (!isConfigValid()) { 1000 | return createJsonResponse({ 1001 | streams: [ 1002 | createErrorStream( 1003 | "Invalid configuration\nEnable and check the logs for more information\nClick for setup instructions" 1004 | ), 1005 | ], 1006 | }); 1007 | } 1008 | 1009 | if (playbackMatch) { 1010 | console.log({ 1011 | message: "Processing playback request", 1012 | fileId: playbackMatch[1], 1013 | range: request.headers.get("Range"), 1014 | }); 1015 | const filename = decodeURIComponent(playbackMatch[2]); 1016 | const fileId = playbackMatch[1]; 1017 | return createProxiedStreamResponse(fileId, filename, request); 1018 | } 1019 | 1020 | const createMetaObject = (id, name, size, thumbnail, createdTime) => ({ 1021 | id: `gdrive:${id}`, 1022 | name, 1023 | posterShape: "landscape", 1024 | background: thumbnail, 1025 | poster: thumbnail, 1026 | description: 1027 | `Size: ${formatSize(size)}` + 1028 | (createdTime 1029 | ? ` | Created: ${new Date(createdTime).toLocaleDateString( 1030 | "en-GB", 1031 | { 1032 | year: "numeric", 1033 | month: "long", 1034 | day: "numeric", 1035 | } 1036 | )}` 1037 | : ""), 1038 | type: "movie", 1039 | }); 1040 | 1041 | if (metaMatch) { 1042 | const fileId = metaMatch[2]; 1043 | if (!fileId) { 1044 | console.error({ 1045 | message: "Failed to extract file ID", 1046 | error: "File ID is undefined", 1047 | }); 1048 | return null; 1049 | } 1050 | const gdriveId = fileId.split(":")[1]; 1051 | const accessToken = await getAccessToken(); 1052 | if (!accessToken) { 1053 | console.error({ 1054 | message: "Failed to get access token", 1055 | error: "Access token is undefined", 1056 | }); 1057 | return null; 1058 | } 1059 | console.log({ message: "Meta request", fileId, gdriveId }); 1060 | const file = await fetchFile(gdriveId, accessToken); 1061 | if (!file) { 1062 | console.error({ 1063 | message: "Failed to fetch file", 1064 | error: "File is undefined", 1065 | }); 1066 | return null; 1067 | } 1068 | console.log({ message: "File fetched", file }); 1069 | const parsedFile = parseFile(file); 1070 | return createJsonResponse({ 1071 | meta: createMetaObject( 1072 | parsedFile.id, 1073 | parsedFile.name, 1074 | parsedFile.size, 1075 | file.thumbnailLink, 1076 | file.createdTime 1077 | ), 1078 | }); 1079 | } 1080 | 1081 | if (catalogMatch) { 1082 | // handle catalogs 1083 | const catalogId = catalogMatch[1]; 1084 | const searchQuery = catalogMatch[2]; 1085 | const searchTerm = searchQuery ? searchQuery.split("=")[1] : null; 1086 | 1087 | console.log({ message: "Catalog request", catalogId, searchTerm }); 1088 | 1089 | if (catalogId === "gdrive_list") { 1090 | // get list of all video files 1091 | const queryParams = { 1092 | q: "mimeType contains 'video/'", 1093 | corpora: "allDrives", 1094 | includeItemsFromAllDrives: "true", 1095 | supportsAllDrives: "true", 1096 | pageSize: "1000", 1097 | orderBy: "createdTime desc", 1098 | fields: "nextPageToken,incompleteSearch,files(id,name,size,videoMediaMetadata,mimeType,fileExtension,thumbnailLink,createdTime)", 1099 | }; 1100 | 1101 | const fetchUrl = new URL(API_ENDPOINTS.DRIVE_FETCH_FILES); 1102 | fetchUrl.search = new URLSearchParams(queryParams).toString(); 1103 | 1104 | const accessToken = await getAccessToken(); 1105 | 1106 | if (!accessToken) { 1107 | return createJsonResponse({ 1108 | error: "Invalid Credentials\nEnable and check the logs for more information\nClick for setup instructions", 1109 | }); 1110 | } 1111 | 1112 | const results = await fetchFiles(fetchUrl, accessToken); 1113 | const metas = results.files.map((file) => 1114 | createMetaObject( 1115 | file.id, 1116 | file.name, 1117 | file.size, 1118 | file.thumbnailLink, 1119 | file.createdTime 1120 | ) 1121 | ); 1122 | console.log({ 1123 | message: "Catalog response", 1124 | numMetas: metas.length, 1125 | }); 1126 | return createJsonResponse({ metas }); 1127 | } 1128 | 1129 | if (catalogId === "gdrive_search") { 1130 | if (!searchTerm) { 1131 | return createJsonResponse({ metas: [] }); 1132 | } 1133 | 1134 | const queryParams = { 1135 | q: buildBaseSearchQuery(decodeURIComponent(searchTerm)), 1136 | corpora: "allDrives", 1137 | includeItemsFromAllDrives: "true", 1138 | supportsAllDrives: "true", 1139 | pageSize: "1000", 1140 | fields: "files(id,name,size,videoMediaMetadata,mimeType,fileExtension,thumbnailLink,createdTime)", 1141 | }; 1142 | 1143 | const fetchUrl = new URL(API_ENDPOINTS.DRIVE_FETCH_FILES); 1144 | fetchUrl.search = new URLSearchParams(queryParams).toString(); 1145 | 1146 | const accessToken = await getAccessToken(); 1147 | 1148 | if (!accessToken) { 1149 | return createJsonResponse({ 1150 | error: "Invalid Credentials\nEnable and check the logs for more information\nClick for setup instructions", 1151 | }); 1152 | } 1153 | 1154 | const results = await fetchFiles(fetchUrl, accessToken); 1155 | 1156 | if (!results?.files || results.files.length === 0) { 1157 | return createJsonResponse({ metas: [] }); 1158 | } 1159 | 1160 | const metas = results.files.map((file) => 1161 | createMetaObject( 1162 | file.id, 1163 | file.name, 1164 | file.size, 1165 | file.thumbnailLink, 1166 | file.createdTime 1167 | ) 1168 | ); 1169 | 1170 | return createJsonResponse({ metas }); 1171 | } 1172 | 1173 | return createJsonResponse({ metas: [] }); 1174 | } 1175 | 1176 | const type = streamMatch[1]; 1177 | 1178 | const fullId = streamMatch[2]; 1179 | let [season, episode] = fullId.split(":").slice(-2); 1180 | console.log({ 1181 | message: "Stream request", 1182 | type, 1183 | fullId, 1184 | season, 1185 | episode, 1186 | }); 1187 | if (fullId.startsWith("kitsu")) { 1188 | season = 1; 1189 | } 1190 | 1191 | if (fullId.startsWith("gdrive")) { 1192 | const fileId = streamMatch[2].split(":")[1]; 1193 | const accessToken = await getAccessToken(); 1194 | if (!accessToken) { 1195 | console.error({ 1196 | message: "Failed to get access token", 1197 | error: "Access token is undefined", 1198 | }); 1199 | return null; 1200 | } 1201 | 1202 | const file = await fetchFile(fileId, accessToken); 1203 | if (!file) { 1204 | console.error({ 1205 | message: "Failed to fetch file", 1206 | error: "File is undefined", 1207 | }); 1208 | return null; 1209 | } 1210 | 1211 | const parsedFile = parseFile(file); 1212 | return createJsonResponse({ 1213 | streams: [createStream(parsedFile, accessToken)], 1214 | }); 1215 | } 1216 | 1217 | const metadata = await getMetadata(type, fullId); 1218 | 1219 | if (!metadata) return createJsonResponse({ streams: [] }); 1220 | 1221 | const parsedStreamRequest = { 1222 | type: type, 1223 | id: fullId, 1224 | season: parseInt(season) || undefined, 1225 | episode: parseInt(episode) || undefined, 1226 | metadata: metadata, 1227 | }; 1228 | 1229 | const streams = await getStreams(parsedStreamRequest); 1230 | 1231 | if (streams.length === 0) { 1232 | return createJsonResponse({ 1233 | streams: [ 1234 | createErrorStream( 1235 | "No streams found\nTry joining more team drives" 1236 | ), 1237 | ], 1238 | }); 1239 | } 1240 | return createJsonResponse({ streams: streams }); 1241 | } catch (error) { 1242 | console.error({ 1243 | message: "An unexpected error occurred", 1244 | error: error.toString(), 1245 | }); 1246 | return new Response("Internal Server Error", { status: 500 }); 1247 | } 1248 | } 1249 | 1250 | async function createProxiedStreamResponse(fileId, filename, request) { 1251 | try { 1252 | const accessToken = await getAccessToken(); 1253 | const streamUrl = API_ENDPOINTS.DRIVE_STREAM_FILE.replace( 1254 | "{fileId}", 1255 | fileId 1256 | ).replace("{filename}", filename); 1257 | 1258 | const headers = { 1259 | Authorization: `Bearer ${accessToken}`, 1260 | Range: request.headers.get("Range") || "bytes=0-", 1261 | }; 1262 | 1263 | const response = await fetch(streamUrl, { headers }); 1264 | if (!response.ok) { 1265 | throw new Error(`Failed to fetch file: ${response.statusText}`); 1266 | } 1267 | 1268 | return new Response(response.body, { 1269 | headers: { 1270 | "Content-Range": response.headers.get("Content-Range"), 1271 | "Content-Length": response.headers.get("Content-Length"), 1272 | }, 1273 | status: response.status, 1274 | statusText: response.statusText, 1275 | }); 1276 | } catch (error) { 1277 | console.error({ 1278 | message: "Failed to create proxied stream response", 1279 | error: error.toString(), 1280 | }); 1281 | return new Response("Internal Server Error", { status: 500 }); 1282 | } 1283 | } 1284 | 1285 | async function getStreams(streamRequest) { 1286 | const streams = []; 1287 | const query = await buildSearchQuery(streamRequest); 1288 | console.log({ message: "Built search query", query, config: CONFIG }); 1289 | 1290 | const queryParams = { 1291 | q: query, 1292 | corpora: "allDrives", 1293 | includeItemsFromAllDrives: "true", 1294 | supportsAllDrives: "true", 1295 | pageSize: "1000", 1296 | fields: "files(id,name,size,videoMediaMetadata,mimeType,fileExtension)", 1297 | }; 1298 | 1299 | const fetchUrl = new URL(API_ENDPOINTS.DRIVE_FETCH_FILES); 1300 | fetchUrl.search = new URLSearchParams(queryParams).toString(); 1301 | 1302 | const accessToken = await getAccessToken(); 1303 | 1304 | if (!accessToken) { 1305 | return [ 1306 | createErrorStream( 1307 | "Invalid Credentials\nEnable and check the logs for more information\nClick for setup instructions" 1308 | ), 1309 | ]; 1310 | } 1311 | 1312 | const results = await fetchFiles(fetchUrl, accessToken); 1313 | 1314 | if (results?.incompleteSearch) { 1315 | console.warn({ message: "The search was incomplete", results }); 1316 | } 1317 | 1318 | if (!results?.files || results.files.length === 0) { 1319 | console.log({ message: "No files found" }); 1320 | return streams; 1321 | } 1322 | 1323 | console.log({ 1324 | message: "Fetched files from Google Drive", 1325 | files: results.files, 1326 | }); 1327 | 1328 | const nameRegex = new RegExp( 1329 | "(? nameRegex.test(file.name)) 1343 | : results.files 1344 | ); 1345 | 1346 | console.log( 1347 | results.files.length - parsedFiles.length === 0 1348 | ? { 1349 | message: `${parsedFiles.length} files successfully parsed`, 1350 | files: parsedFiles, 1351 | } 1352 | : { 1353 | message: `${ 1354 | results.files.length - parsedFiles.length 1355 | } files were filtered out after parsing`, 1356 | filesFiltered: results.files.filter( 1357 | (file) => 1358 | !parsedFiles.some( 1359 | (parsedFile) => parsedFile.id === file.id 1360 | ) 1361 | ), 1362 | config: CONFIG, 1363 | } 1364 | ); 1365 | 1366 | sortParsedFiles(parsedFiles); 1367 | 1368 | console.log({ 1369 | message: "All files parsed, filtered, and sorted successfully", 1370 | files: parsedFiles, 1371 | }); 1372 | 1373 | parsedFiles.forEach((parsedFile) => { 1374 | streams.push(createStream(parsedFile, accessToken)); 1375 | }); 1376 | 1377 | return streams; 1378 | } 1379 | 1380 | export default { 1381 | async fetch(request, env, ctx) { 1382 | CREDENTIALS.clientId = CREDENTIALS.clientId || env.CLIENT_ID; 1383 | CREDENTIALS.clientSecret = 1384 | CREDENTIALS.clientSecret || env.CLIENT_SECRET; 1385 | CREDENTIALS.refreshToken = 1386 | CREDENTIALS.refreshToken || env.REFRESH_TOKEN; 1387 | CONFIG.tmdbApiKey = CONFIG.tmdbApiKey || env.TMDB_API_KEY; 1388 | return handleRequest(request); 1389 | }, 1390 | }; 1391 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "stremio-gdrive" 2 | main = "src/index.js" 3 | compatibility_date = "2024-12-02" 4 | workers_dev = true 5 | preview_urls = true 6 | compatibility_flags = [ "nodejs_compat" ] 7 | [observability.logs] 8 | enabled = true --------------------------------------------------------------------------------