├── .eslintrc.json ├── .gitignore ├── .prettierrc.json ├── .stylelintrc.json ├── LICENSE.md ├── README.md ├── package-lock.json ├── package.json └── src ├── _locales ├── de │ └── messages.json ├── en │ └── messages.json ├── ja │ └── messages.json ├── ko │ └── messages.json ├── outdated_pt_BR │ └── messages.json ├── pl │ └── messages.json ├── ru │ └── messages.json └── sk │ └── messages.json ├── css ├── options.css ├── popup.css └── sidebar.css ├── favicon.ico ├── img ├── icon-dark-16.png ├── icon-dark-48.png ├── icon-dark-96.png ├── icon-dark-enabled-16.png ├── icon-dark-enabled-48.png ├── icon-dark-enabled-96.png ├── icon-light-16.png ├── icon-light-48.png ├── icon-light-96.png ├── icon-light-enabled-16.png ├── icon-light-enabled-48.png └── icon-light-enabled-96.png ├── js ├── background.js ├── components │ ├── defaults.js │ ├── storage.js │ └── supported.js ├── options.js └── popup.js ├── manifest-chrome.json ├── manifest-firefox.json ├── options.html ├── popup.html └── sidebar.html /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["eslint:recommended", "prettier"], 4 | "parser": "@babel/eslint-parser", 5 | "parserOptions": { "requireConfigFile": false }, 6 | "plugins": ["@babel", "prettier"], 7 | "rules": { 8 | "prettier/prettier": [ 9 | "error", 10 | { 11 | "useTabs": true, 12 | "resolveGlobalModules": true, 13 | "singleQuote": false, 14 | "trailingComma": "none" 15 | } 16 | ], 17 | "arrow-body-style": "off", 18 | "prefer-arrow-callback": "off", 19 | "no-unused-vars": 1, 20 | "no-console": 1, 21 | "quotes": [0, "double"], 22 | "react/prop-types": 0, 23 | "no-plusplus": [ 24 | 2, 25 | { 26 | "allowForLoopAfterthoughts": true 27 | } 28 | ], 29 | "eqeqeq": [1, "smart"], 30 | "strict": [0, "global"], 31 | "no-unused-expressions": [ 32 | 2, 33 | { 34 | "allowTernary": true 35 | } 36 | ] 37 | }, 38 | "env": { 39 | "browser": true, 40 | "node": true, 41 | "webextensions": true, 42 | "es6": true 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /dist-chrome 3 | /node_modules 4 | /.parcel-cache 5 | /src/manifest.json 6 | .firefox.exe.lnk 7 | icon.svg 8 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "trailingComma": "none", 4 | "singleQuote": false 5 | } -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-standard" 4 | ], 5 | "rules": { 6 | "indentation": "tab", 7 | "selector-class-pattern": null, 8 | "selector-id-pattern": null 9 | } 10 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## The Stream Detector 2 | 3 | ### What is this? 4 | 5 | This is a Firefox addon written in JavaScript which provides an easy way to keep track of URLs to playlists and subtitles used by Apple HLS, Adobe HDS, MPEG-DASH, and Microsoft Smooth Streaming streams as well as download video/audio files directly and monitor any other file extensions and Content-Type headers. 6 | 7 | Also assembles readymade yt-dlp/FFmpeg/Streamlink/hlsdl/N_m3u8DL-RE commands which (should) include all of the necessary cookies and headers. 8 | 9 | ![A screenshot of the options menu.](https://addons.mozilla.org/user-media/previews/full/274/274526.png) 10 | 11 | More details and screenshots available [in the AMO listing](https://addons.mozilla.org/en-US/firefox/addon/hls-stream-detector/) or the [Web Store listing](https://chrome.google.com/webstore/detail/the-stream-detector/iakkmkmhhckcmoiibcfjnooibphlobak) of the Chrome port. 12 | 13 | ### What is this written in? 14 | 15 | - Javascript, 16 | - WebExtensions API, including: 17 | - Clipboard, 18 | - Downloads, 19 | - Notifications, 20 | - Storage, 21 | - Tabs. 22 | 23 | ### What's the point? 24 | 25 | Being able to easily find direct URLs to streams on the Internet. I wrote this initially for my own use - I was fed up with hunting for URLs in the Network Monitor and manually adding all the necessary headers and cookies. 26 | 27 | ### Is anyone even using this? 28 | 29 | As of updating this document, the addon is at approx. 12,000 average daily users and 2,500 downloads in the last 30 days. 30 | 31 | ### How do I use this? 32 | 33 | Upon being notified that a stream has been detected (as in the screenshot above), click the toolbar button, and then click on the appropriate filename to copy the URL in its desired form. Use the addon's options page to customize your experience and e.g. download media files directly. 34 | 35 | ### Where can I download this? 36 | 37 | - [The Firefox Addons (AMO) listing.](https://addons.mozilla.org/en-US/firefox/addon/hls-stream-detector/) 38 | - [The Chrome Web Store listing.](https://chrome.google.com/webstore/detail/the-stream-detector/iakkmkmhhckcmoiibcfjnooibphlobak) 39 | - [The GitHub releases page.](https://github.com/54ac/stream-detector/releases) 40 | 41 | --- 42 | 43 | ### Additional notes 44 | 45 | - After over 100 closed issues and 60 GitHub releases, as of v2.11, the repo is being archived until further notice due to lack of free time and focusing on work and other projects. Serious issues and minor features might still be addressed, but active development has been put on hold. 46 | - The Chrome version of this addon is not maintained or supported in any way. It's only published on the off chance that it works. Don't expect it to. 47 | - Feel free to submit a translation by way of a pull request if the addon is not available in your preferred language. 48 | - Websites such as YouTube, Vimeo, Facebook, etc. are very likely to use proprietary technologies which are not supported by this addon. When it comes to such "mainstream" services, it's better to use the tools (e.g. yt-dlp) directly. 49 | - This should go without saying, but I am not responsible for and do not condone this addon being used for any nefarious purposes. Copyrighted content is probably DRM-ed anyway. 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stream-detector", 3 | "author": { 4 | "name": "54ac", 5 | "email": "me@54ac.bio", 6 | "url": "https://github.com/54ac" 7 | }, 8 | "repository": "github:54ac/stream-detector", 9 | "version": "1.0.0", 10 | "type": "module", 11 | "private": true, 12 | "devDependencies": { 13 | "@babel/eslint-parser": "^7.18.9", 14 | "@babel/eslint-plugin": "^7.19.1", 15 | "@parcel/babel-preset-env": "^2.8.3", 16 | "@parcel/config-webextension": "^2.8.3", 17 | "cpy-cli": "^5.0.0", 18 | "eslint": "^8.21.0", 19 | "eslint-config-prettier": "^8.5.0", 20 | "eslint-plugin-prettier": "^4.2.1", 21 | "parcel": "^2.8.3", 22 | "prettier": "^3.0.0", 23 | "rimraf": "^5.0.0", 24 | "stylelint": "^15.0.0", 25 | "stylelint-config-recommended": "^13.0.0", 26 | "stylelint-config-standard": "^34.0.0" 27 | }, 28 | "scripts": { 29 | "build": "npm run build-firefox && npm run build-chrome", 30 | "build-firefox": "cpy -u 1 src/manifest-firefox.json . --rename=manifest.json && rimraf dist && parcel build src/manifest.json --config @parcel/config-webextension --no-source-maps && rimraf src/manifest.json", 31 | "build-chrome": "cpy -u 1 src/manifest-chrome.json . --rename=manifest.json && rimraf dist-chrome && parcel build src/manifest.json --config @parcel/config-webextension --no-source-maps --dist-dir=dist-chrome && rimraf src/manifest.json", 32 | "start": "cpy -u 1 src/manifest-firefox.json . --rename=manifest.json && rimraf dist && parcel watch src/manifest.json --host localhost --config @parcel/config-webextension && rimraf src/manifest.json" 33 | }, 34 | "browserslist": "firefox 89" 35 | } 36 | -------------------------------------------------------------------------------- /src/_locales/de/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "autoDownloadPref": { 3 | "message": "Automatisches Herunterladen von Nicht-Manifest-Dateien" 4 | }, 5 | "autoDownloadTip": { 6 | "message": "Speichert Dateien in einem Standardordner. Nützlich, um z.B. einen Stream beim Ansehen zu speichern. Funktioniert nur, wenn 'Ignoriere Links auf Mediendateien' inaktiv ist. Zeigt keine Meldungen, um den Nutzer nicht zu nerven.Nutze URL Blacklists, um automatische Downloads zu verhindern." 7 | }, 8 | "blacklistPref": { 9 | "message": "Nutze Blacklists für Inhalte" 10 | }, 11 | "blacklistTip": { 12 | "message": "Ein Eintrag pro Zeile, Groß-/Kleinschreibung wird ignoriert, betrifft Stream- und Mediendateien, Content-Type Header, und URLs. Das AddOn wird alle URLs ignorieren, die diese Phrasen enthalten.." 13 | }, 14 | "buttonClick": { 15 | "message": "Funktion des Browser Action Buttons" 16 | }, 17 | "clearList": { 18 | "message": "Diese URL Liste leeren" 19 | }, 20 | "copyAll": { 21 | "message": "Kopiere alle sichtbaren URLs" 22 | }, 23 | "copyMethod": { 24 | "message": "Kopiere Stream URL als" 25 | }, 26 | "customCommandPref": { 27 | "message": "Zusätzliche Kommandozeilenparameter" 28 | }, 29 | "customCtPref": { 30 | "message": "Erkenne zusätzliche Content-Type Header" 31 | }, 32 | "customCtTip": { 33 | "message": "Ein Eintrag pro Zeile. Groß-/Kleinschreibung irrelevant." 34 | }, 35 | "customExtPref": { 36 | "message": "Erkenne zusätzliche Dateierweiterungen" 37 | }, 38 | "customExtTip": { 39 | "message": "Ein Eintrag pro Zeile." 40 | }, 41 | "deleteTooltip": { 42 | "message": "Lösche diese URL." 43 | }, 44 | "disablePref": { 45 | "message": "Deaktiviere Erkennung" 46 | }, 47 | "downloadDirectPref": { 48 | "message": "Downloade Nicht-Manifest Dateien anstatt die URL zu kopieren" 49 | }, 50 | "downloaderPref": { 51 | "message": "Nutze externen Downloader" 52 | }, 53 | "exportButton": { 54 | "message": "Export" 55 | }, 56 | "exportSettings": { 57 | "message": "Export/Import der Einstellungen (JSON Datei)" 58 | }, 59 | "extText": { 60 | "message": "Erkennen von Playlists und Untertiteln in HLS, DASH, HDS und MSS Streams. Generiert yt-dlp, FFmpeg, Streamlink, hlsdl und weitere (benutzerdefinierte) Kommandos." 61 | }, 62 | "ffmpeg": { 63 | "message": "FFmpeg Kommando" 64 | }, 65 | "file": { 66 | "message": "Datei" 67 | }, 68 | "fileExtension": { 69 | "message": "Dateierweiterung" 70 | }, 71 | "filePref": { 72 | "message": "Ignoriere Links auf Mediendateien" 73 | }, 74 | "fileSizePref": { 75 | "message": "Ignoriere Mediendateien, wenn kleiner als" 76 | }, 77 | "filenamePref": { 78 | "message": "Nutze den Tab Titel als Dateinamen" 79 | }, 80 | "filterInput": { 81 | "message": "Filter" 82 | }, 83 | "headersPref": { 84 | "message": "Füge zusätzliche Header hinzu" 85 | }, 86 | "hlsdl": { 87 | "message": "hlsdl Kommando" 88 | }, 89 | "importButton": { 90 | "message": "Import" 91 | }, 92 | "importButtonFailure": { 93 | "message": "Einstellungen konnten nicht importiert werden." 94 | }, 95 | "kodiUrl": { 96 | "message": "Kodi URL" 97 | }, 98 | "manifestPref": { 99 | "message": "Ignoriere Streams" 100 | }, 101 | "manifestTip": { 102 | "message": "Eigentlich der Haupt-Anwendungszweck dieses AddONs. Deaktivieren ist sinnvoll, wenn man z.B. ausschließlich Deteien mit einem spezifischen Header oder Extension erfassen möchte." 103 | }, 104 | "multithreadPref": { 105 | "message": "Paralleles Downloaden der fragmente mittels yt-dlp" 106 | }, 107 | "nm3u8dl": { 108 | "message": "N_m3u8DL-RE Kommando" 109 | }, 110 | "noRestorePref": { 111 | "message": "Deaktiviere Wiederaufnahme der vorherigen Session (leere Einträge beim Start)" 112 | }, 113 | "notifCopiedText": { 114 | "message": "Erfolgreich kopierte URL(s):\n" 115 | }, 116 | "notifCopiedTitle": { 117 | "message": "URL(s) kopiert!" 118 | }, 119 | "notifDetectPref": { 120 | "message": "Deaktiviere Meldung beim Erkennen" 121 | }, 122 | "notifDownErrorText": { 123 | "message": "Diese URL verursachte den Fehler:\n" 124 | }, 125 | "notifDownErrorTitle": { 126 | "message": "Downloadfehler!" 127 | }, 128 | "notifErrorText": { 129 | "message": "Kann URL(s) nicht kopieren:\n" 130 | }, 131 | "notifErrorTitle": { 132 | "message": "Kopierfehler!" 133 | }, 134 | "notifIncompCopiedText": { 135 | "message": "Stream URL(s) wurde im Klartext kopiert, da das ausgewählte Tool nicht kompatibel ist:\n" 136 | }, 137 | "notifManyText": { 138 | "message": "Neue Streams erkannt:\n" 139 | }, 140 | "notifManyTitle": { 141 | "message": "Mehrere Streams erkannt!" 142 | }, 143 | "notifPref": { 144 | "message": "Deaktiviere Benachrichtigungen" 145 | }, 146 | "notifText": { 147 | "message": "$1 Stream:\n" 148 | }, 149 | "notifTitle": { 150 | "message": "Neuer Stream erkannt!" 151 | }, 152 | "openOptions": { 153 | "message": "Einstellungen" 154 | }, 155 | "openPopup": { 156 | "message": "Öffne das AddOn Popup" 157 | }, 158 | "openSidebar": { 159 | "message": "Öffne die AddOn Sidebar" 160 | }, 161 | "placeholderCell": { 162 | "message": "Keine URLs verfügbar." 163 | }, 164 | "player": { 165 | "message": "Standard Player" 166 | }, 167 | "popupFilename": { 168 | "message": "Dateiname" 169 | }, 170 | "popupSize": { 171 | "message": "Größe" 172 | }, 173 | "popupSource": { 174 | "message": "Quelle" 175 | }, 176 | "popupTimestamp": { 177 | "message": "Zeitstempel" 178 | }, 179 | "popupType": { 180 | "message": "Typ" 181 | }, 182 | "proxyPref": { 183 | "message": "Nutze HTTP Proxyserver" 184 | }, 185 | "recentPref": { 186 | "message": "Zeige nur eine bestimmte Menge der letzten Einträge" 187 | }, 188 | "regexCommandPref": { 189 | "message": "Nutze regex in benutzerdefinierten Kommandos" 190 | }, 191 | "regexCommandTip": { 192 | "message": "Eingabe als String, z.B. ab+c. Führt Ersetungen im letztendlichen Benutzerkommando durch. Empty replacement field will remove the match." 193 | }, 194 | "regexWarning": { 195 | "message": "Fehlerhafter regulärer Ausdruck." 196 | }, 197 | "resetButton": { 198 | "message": "Einstellungen zurücksetzen" 199 | }, 200 | "resetButtonConfirm": { 201 | "message": "Sicher? Alles auf Standard einstellen?" 202 | }, 203 | "resetButtonLabel": { 204 | "message": "Alle AddOn Daten zurücksetzen (im Notfall)" 205 | }, 206 | "streamlink": { 207 | "message": "Streamlink Kommando" 208 | }, 209 | "streamlinkOutput": { 210 | "message": "Speichere Streamlink zu" 211 | }, 212 | "subtitlePref": { 213 | "message": "Ignoriere Untertitel" 214 | }, 215 | "tabAll": { 216 | "message": "Aktuelle Sitzung" 217 | }, 218 | "tabPrevious": { 219 | "message": "Vorherige Sitzung" 220 | }, 221 | "tabThis": { 222 | "message": "von aktuellem Tab" 223 | }, 224 | "tableForm": { 225 | "message": "Tabelleneintrag" 226 | }, 227 | "timestampPref": { 228 | "message": "Füge Zeitstempel ans Ende der Datei hinzu" 229 | }, 230 | "tipHint": { 231 | "message": "Lass die Maus über einer Option schweben, um mehr über diese zu erfahren." 232 | }, 233 | "titlePref": { 234 | "message": "Zeige den Tab Titel als Quelle" 235 | }, 236 | "url": { 237 | "message": "nur die URL" 238 | }, 239 | "user1": { 240 | "message": "Benutzerdefiniertes Kommando 1" 241 | }, 242 | "user2": { 243 | "message": "Benutzerdefiniertes Kommando 2" 244 | }, 245 | "user3": { 246 | "message": "Benutzerdefiniertes Kommando 3" 247 | }, 248 | "userCommand": { 249 | "message": "benutzerdefiniertes Kommando" 250 | }, 251 | "userCommandTip": { 252 | "message": "Verfügbare Felder:\n%url%\n%filename%\n%useragent%\n%referer%\n%cookie%\n%proxy%\n%origin%\n%tabtitle%\n%timestamp%" 253 | }, 254 | "ytdlp": { 255 | "message": "yt-dlp Kommando" 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "autoDownloadPref": { 3 | "message": "Automatically download non-manifest files" 4 | }, 5 | "autoDownloadTip": { 6 | "message": "Saves files to the default download folder. Useful for e.g. downloading a stream while watching. Works only when 'Ignore direct links to media files' is unchecked. Does not show notifications so as to not spam the user. Use URL blacklist to prevent unwanted downloads." 7 | }, 8 | "blacklistPref": { 9 | "message": "Use content blacklist" 10 | }, 11 | "blacklistTip": { 12 | "message": "One entry per line, case insensitive. Concerns stream/file types, Content-Type headers, and URLs, i.e. the addon will be effectively ignore any URLs involving these phrases." 13 | }, 14 | "buttonClick": { 15 | "message": "Browser action button functionality" 16 | }, 17 | "clearList": { 18 | "message": "Clear this URL list" 19 | }, 20 | "copyAll": { 21 | "message": "Copy all visible URLs" 22 | }, 23 | "copyMethod": { 24 | "message": "Copy stream URL as" 25 | }, 26 | "customCommandPref": { 27 | "message": "Additional command line parameters" 28 | }, 29 | "customCtPref": { 30 | "message": "Detect additional Content-Type headers" 31 | }, 32 | "customCtTip": { 33 | "message": "One entry per line. Case insensitive." 34 | }, 35 | "customExtPref": { 36 | "message": "Detect additional file extensions" 37 | }, 38 | "customExtTip": { 39 | "message": "One entry per line." 40 | }, 41 | "deleteTooltip": { 42 | "message": "Click to delete URL." 43 | }, 44 | "disablePref": { 45 | "message": "Disable detection" 46 | }, 47 | "downloadDirectPref": { 48 | "message": "Download non-manifest files instead of copying their URLs" 49 | }, 50 | "downloaderPref": { 51 | "message": "Use external downloader for yt-dlp" 52 | }, 53 | "exportButton": { 54 | "message": "Export" 55 | }, 56 | "exportSettings": { 57 | "message": "Export/import settings to/from JSON file" 58 | }, 59 | "extText": { 60 | "message": "Detects playlists and subtitles used by streams, incl. custom file extensions and headers. Assembles readymade commands for tools." 61 | }, 62 | "ffmpeg": { 63 | "message": "FFmpeg command" 64 | }, 65 | "file": { 66 | "message": "File" 67 | }, 68 | "fileExtension": { 69 | "message": "File extension" 70 | }, 71 | "filePref": { 72 | "message": "Ignore direct links to media files" 73 | }, 74 | "fileSizePref": { 75 | "message": "Ignore media files smaller than" 76 | }, 77 | "filenamePref": { 78 | "message": "Use tab title as filename" 79 | }, 80 | "filterInput": { 81 | "message": "Filter" 82 | }, 83 | "headersPref": { 84 | "message": "Include additional headers" 85 | }, 86 | "hlsdl": { 87 | "message": "hlsdl command" 88 | }, 89 | "importButton": { 90 | "message": "Import" 91 | }, 92 | "importButtonFailure": { 93 | "message": "Could not import settings." 94 | }, 95 | "kodiUrl": { 96 | "message": "Kodi URL" 97 | }, 98 | "manifestPref": { 99 | "message": "Ignore streams" 100 | }, 101 | "manifestTip": { 102 | "message": "This is the main purpose of the addon. Disable if you want to e.g. only capture files directly or use custom file extensions/content types." 103 | }, 104 | "multithreadPref": { 105 | "message": "Download multiple concurrent fragments in yt-dlp" 106 | }, 107 | "nm3u8dl": { 108 | "message": "N_m3u8DL-RE command" 109 | }, 110 | "noRestorePref": { 111 | "message": "Disable previous session functionality (clear entries on launch)" 112 | }, 113 | "notifCopiedText": { 114 | "message": "Successfully copied the following URL(s):\n" 115 | }, 116 | "notifCopiedTitle": { 117 | "message": "URL(s) copied!" 118 | }, 119 | "notifDetectPref": { 120 | "message": "Disable detection notifications" 121 | }, 122 | "notifDownErrorText": { 123 | "message": "Unable to download URL:\n" 124 | }, 125 | "notifDownErrorTitle": { 126 | "message": "Download error!" 127 | }, 128 | "notifErrorText": { 129 | "message": "Unable to copy URL(s):\n" 130 | }, 131 | "notifErrorTitle": { 132 | "message": "Copy error!" 133 | }, 134 | "notifIncompCopiedText": { 135 | "message": "Stream URL(s) copied in plain form due to lack of compatibility with the chosen tool:\n" 136 | }, 137 | "notifManyText": { 138 | "message": "New streams detected:\n" 139 | }, 140 | "notifManyTitle": { 141 | "message": "Multiple streams detected!" 142 | }, 143 | "notifPref": { 144 | "message": "Disable all notifications" 145 | }, 146 | "notifText": { 147 | "message": "New $1 stream detected:\n" 148 | }, 149 | "notifTitle": { 150 | "message": "Stream detected!" 151 | }, 152 | "openOptions": { 153 | "message": "Options" 154 | }, 155 | "openPopup": { 156 | "message": "Open addon popup" 157 | }, 158 | "openSidebar": { 159 | "message": "Open addon sidebar" 160 | }, 161 | "placeholderCell": { 162 | "message": "No URLs available." 163 | }, 164 | "player": { 165 | "message": "Default player" 166 | }, 167 | "popupFilename": { 168 | "message": "Filename" 169 | }, 170 | "popupSize": { 171 | "message": "Size" 172 | }, 173 | "popupSource": { 174 | "message": "Source" 175 | }, 176 | "popupTimestamp": { 177 | "message": "Timestamp" 178 | }, 179 | "popupType": { 180 | "message": "Type" 181 | }, 182 | "proxyPref": { 183 | "message": "Use HTTP proxy server" 184 | }, 185 | "recentPref": { 186 | "message": "Only show certain number of latest entries" 187 | }, 188 | "regexCommandPref": { 189 | "message": "Use regex in user-defined command" 190 | }, 191 | "regexCommandTip": { 192 | "message": "Enter as string, e.g. ab+c. Performs replacement in final user command. Empty replacement field will remove the match." 193 | }, 194 | "regexWarning": { 195 | "message": "Incorrect regular expression." 196 | }, 197 | "resetButton": { 198 | "message": "Reset settings" 199 | }, 200 | "resetButtonConfirm": { 201 | "message": "Are you sure you wish to reset all settings?" 202 | }, 203 | "resetButtonLabel": { 204 | "message": "Reset all addon data (in case of emergency)" 205 | }, 206 | "streamlink": { 207 | "message": "Streamlink command" 208 | }, 209 | "streamlinkOutput": { 210 | "message": "Output Streamlink to" 211 | }, 212 | "subtitlePref": { 213 | "message": "Ignore subtitles" 214 | }, 215 | "tabAll": { 216 | "message": "Current session" 217 | }, 218 | "tabPrevious": { 219 | "message": "Previous sessions" 220 | }, 221 | "tabThis": { 222 | "message": "Current tab" 223 | }, 224 | "tableForm": { 225 | "message": "Table entry" 226 | }, 227 | "timestampPref": { 228 | "message": "Append timestamp to filename" 229 | }, 230 | "tipHint": { 231 | "message": "Hover over question marks to learn more about particular options." 232 | }, 233 | "titlePref": { 234 | "message": "Show tab title as source" 235 | }, 236 | "url": { 237 | "message": "Regular URL" 238 | }, 239 | "user1": { 240 | "message": "User-defined command 1" 241 | }, 242 | "user2": { 243 | "message": "User-defined command 2" 244 | }, 245 | "user3": { 246 | "message": "User-defined command 3" 247 | }, 248 | "userCommand": { 249 | "message": "User-defined commands" 250 | }, 251 | "userCommandTip": { 252 | "message": "Available fields:\n%url%\n%filename%\n%useragent%\n%referer%\n%cookie%\n%proxy%\n%origin%\n%tabtitle%\n%timestamp%" 253 | }, 254 | "ytdlp": { 255 | "message": "yt-dlp command" 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/_locales/ja/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "autoDownloadPref": { 3 | "message": "自動でnon-manifestファイルをダウンロードする" 4 | }, 5 | "autoDownloadTip": { 6 | "message": "標準のダウンロードフォルダにファイルを保存します。視聴中にストリーミングをダウンロードする場合などに便利です。URLリストへの追加や通知は行いませんので、スパムの心配はありません。不要なダウンロードを防ぐために、URLブラックリストを使用します。" 7 | }, 8 | "blacklistPref": { 9 | "message": "URLブラックリストを使う" 10 | }, 11 | "blacklistTip": { 12 | "message": "1行に1項目です。送信元と送信先のURLの両方が対象です。つまり、アドオンは効果的にこれらのフレーズを含むWebサイトを無効にするだけでなく、ストリームURLも無視します。" 13 | }, 14 | "buttonClick": { 15 | "message": "ブラウザのアクションボタン機能" 16 | }, 17 | "clearList": { 18 | "message": "このURLリストを消去" 19 | }, 20 | "copyAll": { 21 | "message": "表示URLを全てコピー" 22 | }, 23 | "copyMethod": { 24 | "message": "ストリームURLのコピー形式" 25 | }, 26 | "customCommandPref": { 27 | "message": "CLIパラメータを追加する" 28 | }, 29 | "customCtPref": { 30 | "message": "追加のContent-Typeヘッダを検出" 31 | }, 32 | "customCtTip": { 33 | "message": "1行に1項目です。大文字・小文字を区別しません。" 34 | }, 35 | "customExtPref": { 36 | "message": "追加のファイル拡張子を検出" 37 | }, 38 | "customExtTip": { 39 | "message": "1行に1項目です。" 40 | }, 41 | "deleteTooltip": { 42 | "message": "クリックしてこのURLを削除" 43 | }, 44 | "disablePref": { 45 | "message": "検出停止" 46 | }, 47 | "downloadDirectPref": { 48 | "message": "URLをコピーする代わりに、non-manifestファイルをダウンロードする" 49 | }, 50 | "downloaderPref": { 51 | "message": "外部ダウンローダを使う" 52 | }, 53 | "exportButton": { 54 | "message": "エクスポート" 55 | }, 56 | "exportSettings": { 57 | "message": "JSONファイルへ/から設定をエクスポート/インポートする" 58 | }, 59 | "extText": { 60 | "message": "HLS/DASH/HDS/MSSストリームで使用されるプレイリストと字幕、およびカスタムファイル拡張子やContent-Typeヘッダを検出します。既製のyt-dlp/FFmpeg/Streamlink/hlsdl/N_m3u8DL-REのコマンドを組み立てます。" 61 | }, 62 | "ffmpeg": { 63 | "message": "FFmpeg コマンド" 64 | }, 65 | "file": { 66 | "message": "ファイルに保存" 67 | }, 68 | "fileExtension": { 69 | "message": "ファイル拡張子" 70 | }, 71 | "filePref": { 72 | "message": "メディアファイルの直接リンクを無視する" 73 | }, 74 | "fileSizePref": { 75 | "message": "これよりも小さいサイズのメディアファイルを無視する" 76 | }, 77 | "filenamePref": { 78 | "message": "ファイル名にタブのタイトルを使用" 79 | }, 80 | "filterInput": { 81 | "message": "フィルター" 82 | }, 83 | "headersPref": { 84 | "message": "追加のヘッダを含める" 85 | }, 86 | "hlsdl": { 87 | "message": "hlsdl コマンド" 88 | }, 89 | "importButton": { 90 | "message": "インポート" 91 | }, 92 | "importButtonFailure": { 93 | "message": "設定をインポートできません。" 94 | }, 95 | "kodiUrl": { 96 | "message": "Kodi URL" 97 | }, 98 | "manifestPref": { 99 | "message": "ストリームを無視する" 100 | }, 101 | "manifestTip": { 102 | "message": "これはこのアドオンの主目的です。例えば、カスタムのファイル拡張子/Content-Typeのみを使用したい場合は、無効にしてください。" 103 | }, 104 | "multithreadPref": { 105 | "message": "yt-dlpでフラグメントファイルを多重同時ダウンロードする" 106 | }, 107 | "nm3u8dl": { 108 | "message": "N_m3u8DL-RE コマンド" 109 | }, 110 | "noRestorePref": { 111 | "message": "起動時に以前のセッションを無視する" 112 | }, 113 | "notifCopiedText": { 114 | "message": "次のURLのコピーに成功しました:\n" 115 | }, 116 | "notifCopiedTitle": { 117 | "message": "URLをコピーしました!" 118 | }, 119 | "notifDetectPref": { 120 | "message": "検出通知を無効にする" 121 | }, 122 | "notifDownErrorText": { 123 | "message": "URLをダウンロードできません:\n" 124 | }, 125 | "notifDownErrorTitle": { 126 | "message": "ダウンロードエラー!" 127 | }, 128 | "notifErrorText": { 129 | "message": "URLをコピーできません:\n" 130 | }, 131 | "notifErrorTitle": { 132 | "message": "コピーエラー!" 133 | }, 134 | "notifIncompCopiedText": { 135 | "message": "選択したツールとの互換性がないため、ストリームのURLは通常どおりコピーします:\n" 136 | }, 137 | "notifManyText": { 138 | "message": "新たに複数のストリームを検出しました:\n" 139 | }, 140 | "notifManyTitle": { 141 | "message": "複数のストリームを検出しました!" 142 | }, 143 | "notifPref": { 144 | "message": "全通知を無効にする" 145 | }, 146 | "notifText": { 147 | "message": "新たに $1 ストリームを検出しました:\n" 148 | }, 149 | "notifTitle": { 150 | "message": "ストリームを検出しました!" 151 | }, 152 | "openOptions": { 153 | "message": "設定" 154 | }, 155 | "openPopup": { 156 | "message": "アドオンのパネルを開く" 157 | }, 158 | "openSidebar": { 159 | "message": "アドオンのサイドバーを開く" 160 | }, 161 | "placeholderCell": { 162 | "message": "検出したURLがありません" 163 | }, 164 | "player": { 165 | "message": "デフォルトプレーヤで再生" 166 | }, 167 | "popupFilename": { 168 | "message": "ファイル名" 169 | }, 170 | "popupSize": { 171 | "message": "サイズ" 172 | }, 173 | "popupSource": { 174 | "message": "検出ソース" 175 | }, 176 | "popupTimestamp": { 177 | "message": "タイムスタンプ" 178 | }, 179 | "popupType": { 180 | "message": "タイプ" 181 | }, 182 | "proxyPref": { 183 | "message": "HTTPプロキシサーバを使う" 184 | }, 185 | "recentPref": { 186 | "message": "限定する最新エントリーの表示数" 187 | }, 188 | "regexCommandPref": { 189 | "message": "ユーザ定義コマンドで正規表現を使う" 190 | }, 191 | "regexCommandTip": { 192 | "message": "完全な正規表現とオプションのフラグを使用します (例: /ab+c/i)。最終的なユーザーコマンドで置換を行います。置換フィールドが空だと、マッチパターンが削除されます。" 193 | }, 194 | "regexWarning": { 195 | "message": "正規表現が正しくありません。" 196 | }, 197 | "resetButton": { 198 | "message": "全設定のリセット" 199 | }, 200 | "resetButtonConfirm": { 201 | "message": "本当にすべての設定をリセットしますか?" 202 | }, 203 | "resetButtonLabel": { 204 | "message": "全アドオンデータをリセット(緊急用)" 205 | }, 206 | "streamlink": { 207 | "message": "Streamlink コマンド" 208 | }, 209 | "streamlinkOutput": { 210 | "message": "Streamlinkの出力先" 211 | }, 212 | "subtitlePref": { 213 | "message": "字幕を無視する" 214 | }, 215 | "tabAll": { 216 | "message": "全てのタブ" 217 | }, 218 | "tabPrevious": { 219 | "message": "以前のセッション" 220 | }, 221 | "tabThis": { 222 | "message": "このタブ" 223 | }, 224 | "tableForm": { 225 | "message": "テーブルエントリ" 226 | }, 227 | "timestampPref": { 228 | "message": "ファイル名にタイムスタンプを追加" 229 | }, 230 | "tipHint": { 231 | "message": "疑問符にカーソルを合わせると、特定のオプションの詳細を表示します。" 232 | }, 233 | "titlePref": { 234 | "message": "検出ソースにタブのタイトルを表示" 235 | }, 236 | "url": { 237 | "message": "通常のURL" 238 | }, 239 | "user1": { 240 | "message": "ユーザ定義コマンド 1" 241 | }, 242 | "user2": { 243 | "message": "ユーザ定義コマンド 2" 244 | }, 245 | "user3": { 246 | "message": "ユーザ定義コマンド 3" 247 | }, 248 | "userCommand": { 249 | "message": "ユーザ定義コマンド" 250 | }, 251 | "userCommandTip": { 252 | "message": "利用可能な変数:\n%url%\n%filename%\n%useragent%\n%referer%\n%cookie%\n%proxy%\n%origin%\n%tabtitle%\n%timestamp%" 253 | }, 254 | "ytdlp": { 255 | "message": "yt-dlp コマンド" 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/_locales/ko/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "autoDownloadPref": { 3 | "message": "non-manifest 파일 자동 다운로드" 4 | }, 5 | "autoDownloadTip": { 6 | "message": "기본 다운로드 폴더로 파일을 저장합니다. 실시간 스트림을 다운로드할 때 유용합니다. '다이렉트 링크 형식 무시' 옵션이 체크되지 않을 때만 작동합니다. 알림을 표시하지 않습니다. 불필요한 다운로드를 방지하기 위해 블랙리스트 기능을 사용하십시오." 7 | }, 8 | "blacklistPref": { 9 | "message": "컨텐츠 블랙리스트 사용" 10 | }, 11 | "blacklistTip": { 12 | "message": "한 줄에 한 항목씩 입력하세요. 대소문자 구별없음. 스트림/파일 타입, content-type 헤더, URL에 영향을 줍니다. 입력한 문자열을 포함하는 모든 URL을 무시합니다." 13 | }, 14 | "buttonClick": { 15 | "message": "브라우저 액션 버튼 기능" 16 | }, 17 | "clearList": { 18 | "message": "초기화" 19 | }, 20 | "copyAll": { 21 | "message": "모든 URL 복사" 22 | }, 23 | "copyMethod": { 24 | "message": "URL 복사 형식" 25 | }, 26 | "customCommandPref": { 27 | "message": "추가 파라미터" 28 | }, 29 | "customCtPref": { 30 | "message": "감지 규칙 추가 (content-type 헤더)" 31 | }, 32 | "customCtTip": { 33 | "message": "한 줄에 한 항목씩 입력하세요. 대소문자 구별없음." 34 | }, 35 | "customExtPref": { 36 | "message": "감지 규칙 추가 (파일 확장자)" 37 | }, 38 | "customExtTip": { 39 | "message": "한 줄에 한 항목씩 입력하세요." 40 | }, 41 | "deleteTooltip": { 42 | "message": "이 URL을 목록에서 제거합니다." 43 | }, 44 | "disablePref": { 45 | "message": "감지 기능 비활성화" 46 | }, 47 | "downloadDirectPref": { 48 | "message": "URL을 복사하지 않고 non-manifest 파일 다운로드" 49 | }, 50 | "downloaderPref": { 51 | "message": "외부 다운로더 사용 (yt-dlp)" 52 | }, 53 | "exportButton": { 54 | "message": "내보내기" 55 | }, 56 | "exportSettings": { 57 | "message": "JSON 형식으로 설정값 내보내기/불러오기" 58 | }, 59 | "extText": { 60 | "message": "HLS/DASH/HDS/MSS 스트림 형태의 영상과 자막 주소를 추출하고 다운로드할 수 있는 부가기능입니다. 사용자 지정 파일 확장자와 content-type 헤더 또한 지정할 수 있습니다. yt-dlp/FFmpeg/Streamlink/hlsdl/N_m3u8DL-RE 프리셋을 제공하여 명령어를 일일이 작성할 필요가 없습니다." 61 | }, 62 | "ffmpeg": { 63 | "message": "FFmpeg 명령어" 64 | }, 65 | "file": { 66 | "message": "파일로 저장" 67 | }, 68 | "fileExtension": { 69 | "message": "파일 확장자" 70 | }, 71 | "filePref": { 72 | "message": "다이렉트 링크 형식 무시" 73 | }, 74 | "fileSizePref": { 75 | "message": "다음 크기 미만의 영상 파일은 무시합니다" 76 | }, 77 | "filenamePref": { 78 | "message": "탭 제목을 파일명으로 사용" 79 | }, 80 | "filterInput": { 81 | "message": "필터" 82 | }, 83 | "headersPref": { 84 | "message": "헤더 추가하기" 85 | }, 86 | "hlsdl": { 87 | "message": "hlsdl 명령어" 88 | }, 89 | "importButton": { 90 | "message": "불러오기" 91 | }, 92 | "importButtonFailure": { 93 | "message": "설정값을 불러올 수 없습니다." 94 | }, 95 | "kodiUrl": { 96 | "message": "Kodi URL" 97 | }, 98 | "manifestPref": { 99 | "message": "스트림 무시하기" 100 | }, 101 | "manifestTip": { 102 | "message": "이 설정값은 본 부가 기능의 주요 기능을 비활성화합니다. 확실히 알고 있는 경우에만 이용해 주시기 바랍니다. 예시, 특정 파일 확장자/컨텐츠 타입만 감지하고 싶은 경우" 103 | }, 104 | "multithreadPref": { 105 | "message": "동시 다운로드 분할 수 (yt-dlp)" 106 | }, 107 | "nm3u8dl": { 108 | "message": "N_m3u8DL-RE 명령어" 109 | }, 110 | "noRestorePref": { 111 | "message": "이전 세션 기능 비활성화 (시작 시 초기화)" 112 | }, 113 | "notifCopiedText": { 114 | "message": "성공적으로 클립보드에 복사됐습니다:\n" 115 | }, 116 | "notifCopiedTitle": { 117 | "message": "URL 복사 성공!" 118 | }, 119 | "notifDetectPref": { 120 | "message": "감지 알림 끄기" 121 | }, 122 | "notifDownErrorText": { 123 | "message": "URL을 다운로드할 수 없습니다:\n" 124 | }, 125 | "notifDownErrorTitle": { 126 | "message": "다운로드 에러!" 127 | }, 128 | "notifErrorText": { 129 | "message": "URL을 복사할 수 없습니다:\n" 130 | }, 131 | "notifErrorTitle": { 132 | "message": "복사 에러!" 133 | }, 134 | "notifIncompCopiedText": { 135 | "message": "선택한 옵션과는 호환되지 않아 원본 스트림 URL로 복사됐습니다:\n" 136 | }, 137 | "notifManyText": { 138 | "message": "새로운 스트림이 감지됐습니다:\n" 139 | }, 140 | "notifManyTitle": { 141 | "message": "스트림 감지 성공!" 142 | }, 143 | "notifPref": { 144 | "message": "모든 알림 끄기" 145 | }, 146 | "notifText": { 147 | "message": "새로운 $1 스트림 주소가 감지됐습니다:\n" 148 | }, 149 | "notifTitle": { 150 | "message": "스트림 감지 성공!" 151 | }, 152 | "openOptions": { 153 | "message": "옵션" 154 | }, 155 | "openPopup": { 156 | "message": "부가 기능 팝업 열기" 157 | }, 158 | "openSidebar": { 159 | "message": "부가 기능 사이드바 열기" 160 | }, 161 | "placeholderCell": { 162 | "message": "목록 없음" 163 | }, 164 | "player": { 165 | "message": "플레이어로 재생" 166 | }, 167 | "popupFilename": { 168 | "message": "파일명" 169 | }, 170 | "popupSize": { 171 | "message": "크기" 172 | }, 173 | "popupSource": { 174 | "message": "출처" 175 | }, 176 | "popupTimestamp": { 177 | "message": "시간" 178 | }, 179 | "popupType": { 180 | "message": "종류" 181 | }, 182 | "proxyPref": { 183 | "message": "HTTP 프록시 서버 사용" 184 | }, 185 | "recentPref": { 186 | "message": "표시 항목 수 제한" 187 | }, 188 | "regexCommandPref": { 189 | "message": "사용자 정의 명령어에 regex 사용" 190 | }, 191 | "regexCommandTip": { 192 | "message": "string(문자열)로 입력하세요. 예시, ab+c. 최종 사용자 명령에서 교체(replacement)를 수행합니다. 교체 필드가 비어 있으면 매칭되는 항목은 제거됩니다." 193 | }, 194 | "regexWarning": { 195 | "message": "잘못된 정규 표현식." 196 | }, 197 | "resetButton": { 198 | "message": "설정 초기화" 199 | }, 200 | "resetButtonConfirm": { 201 | "message": "정말로 모든 설정을 초기화하시겠습니까?" 202 | }, 203 | "resetButtonLabel": { 204 | "message": "모든 부가 기능 데이터 초기화 (긴급 상황 시)" 205 | }, 206 | "streamlink": { 207 | "message": "Streamlink 명령어" 208 | }, 209 | "streamlinkOutput": { 210 | "message": "Streamlink 저장 옵션" 211 | }, 212 | "subtitlePref": { 213 | "message": "자막 무시하기" 214 | }, 215 | "tabAll": { 216 | "message": "현재 세션" 217 | }, 218 | "tabPrevious": { 219 | "message": "이전 세션" 220 | }, 221 | "tabThis": { 222 | "message": "현재 탭" 223 | }, 224 | "tableForm": { 225 | "message": "표 형식" 226 | }, 227 | "timestampPref": { 228 | "message": "파일명에 시간 추가" 229 | }, 230 | "tipHint": { 231 | "message": "물음표 위에 마우스를 올려놓으면 특정 옵션에 대한 설명을 확인할 수 있습니다." 232 | }, 233 | "titlePref": { 234 | "message": "출처 칸에 URL 대신 탭 제목 사용" 235 | }, 236 | "url": { 237 | "message": "원본 URL" 238 | }, 239 | "user1": { 240 | "message": "사용자 정의 명령어 1" 241 | }, 242 | "user2": { 243 | "message": "사용자 정의 명령어 2" 244 | }, 245 | "user3": { 246 | "message": "사용자 정의 명령어 3" 247 | }, 248 | "userCommand": { 249 | "message": "사용자 정의 명령어" 250 | }, 251 | "userCommandTip": { 252 | "message": "예시:\n%url%\n%filename%\n%useragent%\n%referer%\n%cookie%\n%proxy%\n%origin%\n%tabtitle%\n%timestamp%" 253 | }, 254 | "ytdlp": { 255 | "message": "yt-dlp 명령어" 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/_locales/outdated_pt_BR/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "notifTitle": { 3 | "message": "Stream detectado!" 4 | }, 5 | "notifText": { 6 | "message": "$1 novo stream detectado:\n" 7 | }, 8 | "notifCopiedTitle": { 9 | "message": "URL(s) copiada(s)!" 10 | }, 11 | "notifCopiedText": { 12 | "message": "URL(s) copiada(s) com sucesso:\n" 13 | }, 14 | "notifIncompCopiedText": { 15 | "message": "URL(s) streams copiado(s) no formato simples devido à falta de compatibilidade com a ferramenta escolhida:\n" 16 | }, 17 | "notifErrorTitle": { 18 | "message": "Erro ao copiar!" 19 | }, 20 | "notifErrorText": { 21 | "message": "Não foi possível copiar a(s) URL(s):\n" 22 | }, 23 | "extText": { 24 | "message": "Detecta listas de reprodução e legendas usadas por HLS/DASH/HDS/MSS streams, assim como extensões de arquivos personalizados e cabeçalhos tipo Content-Type. Cria comandos prontos para yt-dlp/FFmpeg/Streamlink/hlsdl/N_m3u8DL-RE." 25 | }, 26 | "copyMethod": { 27 | "message": "Copiar a URL do stream como" 28 | }, 29 | "url": { 30 | "message": "URL normal" 31 | }, 32 | "kodiUrl": { 33 | "message": "URL Kodi" 34 | }, 35 | "tableForm": { 36 | "message": "Entrada de tabela" 37 | }, 38 | "ytdlp": { 39 | "message": "comando yt-dlp" 40 | }, 41 | "ffmpeg": { 42 | "message": "comando FFmpeg" 43 | }, 44 | "streamlink": { 45 | "message": "comando Streamlink" 46 | }, 47 | "hlsdl": { 48 | "message": "comando hlsdl" 49 | }, 50 | "nm3u8dl": { 51 | "message": "comando N_m3u8DL-RE" 52 | }, 53 | "user": { 54 | "message": "Comando definido por usuário" 55 | }, 56 | "streamlinkOutput": { 57 | "message": "Enviar Streamlink para" 58 | }, 59 | "file": { 60 | "message": "Arquivo" 61 | }, 62 | "player": { 63 | "message": "Reprodutor padrão" 64 | }, 65 | "customCommandPref": { 66 | "message": "Parâmetros adicionais da linha de comando" 67 | }, 68 | "userCommand": { 69 | "message": "Comando definido por usuário" 70 | }, 71 | "userCommandTip": { 72 | "message": "Campos disponíveis:\n%url%\n%filename%\n%useragent%\n%referer%\n%cookie%\n%proxy%\n%origin%\n%tabtitle%\n%timestamp%" 73 | }, 74 | "headersPref": { 75 | "message": "Incluir cabeçalhos adicionais" 76 | }, 77 | "notifDetectPref": { 78 | "message": "Desativar notificações de detecção" 79 | }, 80 | "notifPref": { 81 | "message": "Desativar todas notificações" 82 | }, 83 | "disablePref": { 84 | "message": "Desativar detecção" 85 | }, 86 | "clearList": { 87 | "message": "Limpar esta lista de URL" 88 | }, 89 | "copyAll": { 90 | "message": "Copiar todas URLs visíveis" 91 | }, 92 | "proxyPref": { 93 | "message": "Usar servidor proxy de HTTP" 94 | }, 95 | "downloaderPref": { 96 | "message": "Usar downloader externo" 97 | }, 98 | "buttonClick": { 99 | "message": "Funcionalidade do botão de ação no navegador" 100 | }, 101 | "popupType": { 102 | "message": "Tipo" 103 | }, 104 | "popupFilename": { 105 | "message": "Nome do arquivo" 106 | }, 107 | "popupSource": { 108 | "message": "Fonte" 109 | }, 110 | "popupTimestamp": { 111 | "message": "Registro de data" 112 | }, 113 | "deleteTooltip": { 114 | "message": "Clique para remover a URL." 115 | }, 116 | "tabThis": { 117 | "message": "Esta aba" 118 | }, 119 | "tabAll": { 120 | "message": "Todas abas" 121 | }, 122 | "tabPrevious": { 123 | "message": "Sessões anteriores " 124 | }, 125 | "filterInput": { 126 | "message": "Filtro" 127 | }, 128 | "openOptions": { 129 | "message": "Opções" 130 | }, 131 | "titlePref": { 132 | "message": "Mostrar o título da aba como fonte" 133 | }, 134 | "filenamePref": { 135 | "message": "Usar o título da aba como nome de arquivo" 136 | }, 137 | "timestampPref": { 138 | "message": "Anexar registro com data ao nome do arquivo" 139 | }, 140 | "fileExtension": { 141 | "message": "Extensão do arquivo" 142 | }, 143 | "subtitlePref": { 144 | "message": "Ignorar legendas" 145 | }, 146 | "filePref": { 147 | "message": "Ignorar links diretos para arquivos de vídeo/áudio" 148 | }, 149 | "manifestPref": { 150 | "message": "Ignorar streams" 151 | }, 152 | "manifestTip": { 153 | "message": "Esta é a principal função da extensão. Desative isto se você quiser, por exemplo, usar apenas extensões de arquivos/conteúdo personalizados." 154 | }, 155 | "openPopup": { 156 | "message": "Abrir popup da extensão" 157 | }, 158 | "placeholderCell": { 159 | "message": "Nenhuma URL disponível." 160 | }, 161 | "blacklistPref": { 162 | "message": "Use a URL com lista de não autorizados" 163 | }, 164 | "blacklistTip": { 165 | "message": "Uma entrada por linha. Refere-se tanto às URLs de origem como de destino, ou seja, a extensão será efetivamente desativada em qualquer site, além de ignorar qualquer URL de stream que contenha estas frases." 166 | }, 167 | "customExtPref": { 168 | "message": "Detectar extensões de arquivos adicionais" 169 | }, 170 | "customExtTip": { 171 | "message": "Uma entrada por linha." 172 | }, 173 | "customCtPref": { 174 | "message": "Detectar cabeçalhos adicionais do tipo Content-Type" 175 | }, 176 | "customCtTip": { 177 | "message": "Uma entrada por linha. Indiferente a maiúsculas e minúsculas." 178 | }, 179 | "cleanupPref": { 180 | "message": "Remover entradas com mais de uma semana" 181 | }, 182 | "cleanupTip": { 183 | "message": "Aplicado somente no reinício em caso de falhas." 184 | }, 185 | "resetButtonLabel": { 186 | "message": "Resetar todas as configurações (em caso de emergência)" 187 | }, 188 | "resetButton": { 189 | "message": "Resetar configurações" 190 | }, 191 | "resetButtonConfirm": { 192 | "message": "Você tem certeza que quer resetar todas as configurações?" 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/_locales/pl/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "autoDownloadPref": { 3 | "message": "Automatycznie ściągaj multimedia" 4 | }, 5 | "autoDownloadTip": { 6 | "message": "Zapisuje pliki do domyślnego folderu. Przydatne np. do pobierania streamu podczas oglądania. Działa jedynie, gdy opcja 'Nie wykrywaj bezpośrednich linków do multimediów' jest wyłączona. Nie pokazuje powiadomień, aby nie spamować użytkownika.Użyj filtra linków, by zapobiec pobieraniu niepotrzebnych plików." 7 | }, 8 | "blacklistPref": { 9 | "message": "Filtr danych" 10 | }, 11 | "blacklistTip": { 12 | "message": "Każdy wpis w nowej linii, ignoruje wielkość liter. Dotyczy typów streamów/plików, nagłówków Content-Type oraz linków - dodatek nie będzie wykrywał linków na pasujących stronach oraz zignoruje linki związane z danymi frazami." 13 | }, 14 | "buttonClick": { 15 | "message": "Działanie przycisku na pasku narzędzi" 16 | }, 17 | "clearList": { 18 | "message": "Wyczyść tę listę" 19 | }, 20 | "copyAll": { 21 | "message": "Skopiuj widoczne linki" 22 | }, 23 | "copyMethod": { 24 | "message": "Skopiuj link do streamu jako" 25 | }, 26 | "customCommandPref": { 27 | "message": "Dodatkowe argumenty linii poleceń" 28 | }, 29 | "customCtPref": { 30 | "message": "Wykrywaj dodatkowe nagłówki Content-Type" 31 | }, 32 | "customCtTip": { 33 | "message": "Każdy wpis w nowej linii. Wielkość liter nieistotna." 34 | }, 35 | "customExtPref": { 36 | "message": "Wykrywaj dodatkowe rozszerzenia plików" 37 | }, 38 | "customExtTip": { 39 | "message": "Każdy wpis w nowej linii." 40 | }, 41 | "deleteTooltip": { 42 | "message": "Kliknij, aby usunąć link." 43 | }, 44 | "disablePref": { 45 | "message": "Wyłącz wykrywanie" 46 | }, 47 | "downloadDirectPref": { 48 | "message": "Ściągaj pliki bezpośrednio zamiast kopiować ich linki (oprócz manifestów)" 49 | }, 50 | "downloaderPref": { 51 | "message": "Użyj zewnętrznego narzędzia pobierania z yt-dlp" 52 | }, 53 | "exportButton": { 54 | "message": "Eksport" 55 | }, 56 | "exportSettings": { 57 | "message": "Eksport/import ustawień do/z pliku JSON" 58 | }, 59 | "extText": { 60 | "message": "Wykrywa playlisty i napisy używane przez streamy oraz własne rozszerzenia plików i nagłówki. Tworzy gotowe komendy dla programów." 61 | }, 62 | "ffmpeg": { 63 | "message": "Komendę FFmpeg" 64 | }, 65 | "file": { 66 | "message": "Pliku" 67 | }, 68 | "fileExtension": { 69 | "message": "Rozszerzenie pliku" 70 | }, 71 | "filePref": { 72 | "message": "Nie wykrywaj bezpośrednich linków do multimediów" 73 | }, 74 | "fileSizePref": { 75 | "message": "Ignoruj pliki multimedialne o wielkości poniżej" 76 | }, 77 | "filenamePref": { 78 | "message": "Tytuł karty jako nazwa pliku" 79 | }, 80 | "filterInput": { 81 | "message": "Filtr" 82 | }, 83 | "headersPref": { 84 | "message": "Dołącz dodatkowe nagłówki" 85 | }, 86 | "hlsdl": { 87 | "message": "Komendę hlsdl" 88 | }, 89 | "importButton": { 90 | "message": "Import" 91 | }, 92 | "importButtonFailure": { 93 | "message": "Nie udało się zaimportować ustawień." 94 | }, 95 | "kodiUrl": { 96 | "message": "Link Kodi" 97 | }, 98 | "manifestPref": { 99 | "message": "Nie wykrywaj streamów" 100 | }, 101 | "manifestTip": { 102 | "message": "Główny cel tego dodatku. Wyłącz, jeśli chcesz np. wykrywać pliki bezpośrednio lub wyłącznie używać własnych rozszerzeń/nagłówków content type." 103 | }, 104 | "multithreadPref": { 105 | "message": "Pobieraj wiele fragmentów równocześnie w yt-dlp" 106 | }, 107 | "nm3u8dl": { 108 | "message": "Komendę N_m3u8DL-RE" 109 | }, 110 | "noRestorePref": { 111 | "message": "Wyłącz obsługę poprzednich sesji (usuń wpisy przy starcie)" 112 | }, 113 | "notifCopiedText": { 114 | "message": "Pomyślnie skopiowano następujące link(i) do schowka:\n" 115 | }, 116 | "notifCopiedTitle": { 117 | "message": "Skopiowano link(i)!" 118 | }, 119 | "notifDetectPref": { 120 | "message": "Wyłącz powiadomienia o wykryciu linków" 121 | }, 122 | "notifDownErrorText": { 123 | "message": "Nie udało się ściągnąć pliku:\n" 124 | }, 125 | "notifDownErrorTitle": { 126 | "message": "Błąd ściągania!" 127 | }, 128 | "notifErrorText": { 129 | "message": "Nie udało się skopiować linków:\n" 130 | }, 131 | "notifErrorTitle": { 132 | "message": "Błąd kopiowania!" 133 | }, 134 | "notifIncompCopiedText": { 135 | "message": "Dane skopiowane w zwykłej formie z powodu użycia narzędzia niekompatybilnego z rodzajem streamu:\n" 136 | }, 137 | "notifManyText": { 138 | "message": "Wykryto nowe streamy:\n" 139 | }, 140 | "notifManyTitle": { 141 | "message": "Wykryto streamy!" 142 | }, 143 | "notifPref": { 144 | "message": "Wyłącz wszystkie powiadomienia" 145 | }, 146 | "notifText": { 147 | "message": "Wykryto nowy stream $1:\n" 148 | }, 149 | "notifTitle": { 150 | "message": "Wykryto stream!" 151 | }, 152 | "openOptions": { 153 | "message": "Ustawienia" 154 | }, 155 | "openPopup": { 156 | "message": "Otwórz główne okno" 157 | }, 158 | "openSidebar": { 159 | "message": "Otwórz panel boczny" 160 | }, 161 | "placeholderCell": { 162 | "message": "Brak dostępnych linków." 163 | }, 164 | "player": { 165 | "message": "Domyślnego odtwarzacza" 166 | }, 167 | "popupFilename": { 168 | "message": "Nazwa pliku" 169 | }, 170 | "popupSize": { 171 | "message": "Rozmiar" 172 | }, 173 | "popupSource": { 174 | "message": "Źródło" 175 | }, 176 | "popupTimestamp": { 177 | "message": "Data" 178 | }, 179 | "popupType": { 180 | "message": "Rodzaj" 181 | }, 182 | "proxyPref": { 183 | "message": "Użyj serwera HTTP proxy" 184 | }, 185 | "recentPref": { 186 | "message": "Wyświetl jedynie określoną liczbę ostatnich wpisów" 187 | }, 188 | "regexCommandPref": { 189 | "message": "Użyj regex w komendzie własnej" 190 | }, 191 | "regexCommandTip": { 192 | "message": "Wpisz jako ciąg znaków, np. ab+c. Wykonuje zamianę w ostatecznej formie komendy własnej. Puste pole zamiany oznacza usunięcie." 193 | }, 194 | "regexWarning": { 195 | "message": "Nieprawidłowe wyrażenie regularne." 196 | }, 197 | "resetButton": { 198 | "message": "Reset ustawień" 199 | }, 200 | "resetButtonConfirm": { 201 | "message": "Czy na pewno chcesz zresetować ustawienia?" 202 | }, 203 | "resetButtonLabel": { 204 | "message": "Reset wszystkich danych dodatku (w przypadku awarii)" 205 | }, 206 | "streamlink": { 207 | "message": "Komendę Streamlink" 208 | }, 209 | "streamlinkOutput": { 210 | "message": "Przesyłaj strumień wyjściowy Streamlink do" 211 | }, 212 | "subtitlePref": { 213 | "message": "Nie wykrywaj napisów" 214 | }, 215 | "tabAll": { 216 | "message": "Obecna sesja" 217 | }, 218 | "tabPrevious": { 219 | "message": "Poprzednie sesje" 220 | }, 221 | "tabThis": { 222 | "message": "Obecna karta" 223 | }, 224 | "tableForm": { 225 | "message": "Wpis tabeli" 226 | }, 227 | "timestampPref": { 228 | "message": "Dodaj datę i czas do nazwy pliku" 229 | }, 230 | "tipHint": { 231 | "message": "Najedź myszką na znaki zapytania, aby dowiedzieć się więcej o poszczególnych opcjach." 232 | }, 233 | "titlePref": { 234 | "message": "Pokaż tytuł karty jako źródło" 235 | }, 236 | "url": { 237 | "message": "Zwykły link" 238 | }, 239 | "user1": { 240 | "message": "Komendę własną 1" 241 | }, 242 | "user2": { 243 | "message": "Komendę własną 2" 244 | }, 245 | "user3": { 246 | "message": "Komendę własną 3" 247 | }, 248 | "userCommand": { 249 | "message": "Komendy własne" 250 | }, 251 | "userCommandTip": { 252 | "message": "Dostępne pola:\n%url%\n%filename%\n%useragent%\n%referer%\n%cookie%\n%proxy%\n%origin%\n%tabtitle%\n%timestamp%" 253 | }, 254 | "ytdlp": { 255 | "message": "Komendę yt-dlp" 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/_locales/ru/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "autoDownloadPref": { 3 | "message": "Автоматически загружать медиафайлы" 4 | }, 5 | "autoDownloadTip": { 6 | "message": "Сохранять файлы в папку загрузок по умолчанию. Полезно, например, для загрузки потока во время просмотра. Работает только при отключённой функции «Игнорировать прямые ссылки на медиафайлы». Уведомления не отображаются, чтобы не утомлять пользователя. Используйте фильтр для предотвращения нежелательных загрузок." 7 | }, 8 | "blacklistPref": { 9 | "message": "Использовать фильтр содержимого" 10 | }, 11 | "blacklistTip": { 12 | "message": "По одной записи в строке, регистр не учитывается. Касается типов потоков/файлов, заголовков типа содержимого и ссылок, т.е. расширение будет эффективно игнорировать любые ссылки, содержащие эти фразы." 13 | }, 14 | "buttonClick": { 15 | "message": "Функция кнопки действия браузера" 16 | }, 17 | "clearList": { 18 | "message": "Очистить список" 19 | }, 20 | "copyAll": { 21 | "message": "Скопировать все ссылки" 22 | }, 23 | "copyMethod": { 24 | "message": "Копировать ссылку на поток как" 25 | }, 26 | "customCommandPref": { 27 | "message": "Дополнительные параметры командной строки" 28 | }, 29 | "customCtPref": { 30 | "message": "Поиск дополнительных заголовков типа содержимого" 31 | }, 32 | "customCtTip": { 33 | "message": "По одной записи в строке. Регистр не учитывается." 34 | }, 35 | "customExtPref": { 36 | "message": "Поиск дополнительных расширений файлов" 37 | }, 38 | "customExtTip": { 39 | "message": "По одной записи в строке." 40 | }, 41 | "deleteTooltip": { 42 | "message": "Нажмите, чтобы удалить ссылку" 43 | }, 44 | "disablePref": { 45 | "message": "Отключить поиск" 46 | }, 47 | "downloadDirectPref": { 48 | "message": "Загружать медиафайлы вместо копирования их ссылок" 49 | }, 50 | "downloaderPref": { 51 | "message": "Использовать внешний загрузчик" 52 | }, 53 | "exportButton": { 54 | "message": "Экспорт" 55 | }, 56 | "exportSettings": { 57 | "message": "Экспорт/импорт настроек через JSON-файл" 58 | }, 59 | "extText": { 60 | "message": "Находит списки воспроизведения и субтитры, используемые потоками HLS/DASH/HDS/MSS, а также пользовательские расширения файлов и заголовки типов содержимого. Формирует готовые командные строки для yt-dlp/FFmpeg/Streamlink/hlsdl/N_m3u8DL-RE." 61 | }, 62 | "ffmpeg": { 63 | "message": "Команда для FFmpeg" 64 | }, 65 | "file": { 66 | "message": "Файл" 67 | }, 68 | "fileExtension": { 69 | "message": "Расширение файла" 70 | }, 71 | "filePref": { 72 | "message": "Игнорировать прямые ссылки на медиафайлы" 73 | }, 74 | "fileSizePref": { 75 | "message": "Игнорировать медиафайлы менее" 76 | }, 77 | "filenamePref": { 78 | "message": "Использовать заголовок вкладки как имя файла" 79 | }, 80 | "filterInput": { 81 | "message": "Фильтр" 82 | }, 83 | "headersPref": { 84 | "message": "Включать дополнительные заголовки" 85 | }, 86 | "hlsdl": { 87 | "message": "Команда для hlsdl" 88 | }, 89 | "importButton": { 90 | "message": "Импорт" 91 | }, 92 | "importButtonFailure": { 93 | "message": "Невозможно импортировать настройки." 94 | }, 95 | "kodiUrl": { 96 | "message": "Ссылка для Kodi" 97 | }, 98 | "manifestPref": { 99 | "message": "Игнорировать потоки" 100 | }, 101 | "manifestTip": { 102 | "message": "Это основная цель расширения. Отключайте, если хотите, например, использовать только пользовательские расширения файлов/типы содержимого." 103 | }, 104 | "multithreadPref": { 105 | "message": "Загружать несколько фрагментов параллельно в yt-dlp" 106 | }, 107 | "nm3u8dl": { 108 | "message": "Команда для N_m3u8DL-RE" 109 | }, 110 | "noRestorePref": { 111 | "message": "Отключить функцию предыдущей сессии (очищать записи при запуске)" 112 | }, 113 | "notifCopiedText": { 114 | "message": "Скопированы следующее ссылки:\n" 115 | }, 116 | "notifCopiedTitle": { 117 | "message": "Скопировано!" 118 | }, 119 | "notifDetectPref": { 120 | "message": "Отключить уведомления обнаружения" 121 | }, 122 | "notifDownErrorText": { 123 | "message": "Невозможно загрузить по ссылке:\n" 124 | }, 125 | "notifDownErrorTitle": { 126 | "message": "Ошибка загрузки!" 127 | }, 128 | "notifErrorText": { 129 | "message": "Невозможно скопировать ссылки:\n" 130 | }, 131 | "notifErrorTitle": { 132 | "message": "Ошибка копирования!" 133 | }, 134 | "notifIncompCopiedText": { 135 | "message": "Ссылки на потоки скопированы как обычные из-за несовместимости с выбранной программой:\n" 136 | }, 137 | "notifManyText": { 138 | "message": "Найдены новые потоки:\n" 139 | }, 140 | "notifManyTitle": { 141 | "message": "Найдено несколько потоков!" 142 | }, 143 | "notifPref": { 144 | "message": "Отключить все уведомления" 145 | }, 146 | "notifText": { 147 | "message": "Найден новый $1-поток:\n" 148 | }, 149 | "notifTitle": { 150 | "message": "Найден поток!" 151 | }, 152 | "openOptions": { 153 | "message": "Настройки" 154 | }, 155 | "openPopup": { 156 | "message": "Открыть панель расширения" 157 | }, 158 | "openSidebar": { 159 | "message": "Открыть боковую панель" 160 | }, 161 | "placeholderCell": { 162 | "message": "Ссылки не найдены." 163 | }, 164 | "player": { 165 | "message": "Проигрыватель по умолчанию" 166 | }, 167 | "popupFilename": { 168 | "message": "Имя файла" 169 | }, 170 | "popupSize": { 171 | "message": "Размер" 172 | }, 173 | "popupSource": { 174 | "message": "Источник" 175 | }, 176 | "popupTimestamp": { 177 | "message": "Дата/время" 178 | }, 179 | "popupType": { 180 | "message": "Тип" 181 | }, 182 | "proxyPref": { 183 | "message": "Использовать HTTP-прокси" 184 | }, 185 | "recentPref": { 186 | "message": "Показывать определённое количество последних записей" 187 | }, 188 | "regexCommandPref": { 189 | "message": "Регулярное выражение в пользовательской команде" 190 | }, 191 | "regexCommandTip": { 192 | "message": "Введите как строку, например: ab+c. Выполняется поиск и замена в конечной пользовательской команде. Пустое поле замены удаляет найденное совпадение." 193 | }, 194 | "regexWarning": { 195 | "message": "Неправильное регулярное выражение." 196 | }, 197 | "resetButton": { 198 | "message": "Сброс настроек" 199 | }, 200 | "resetButtonConfirm": { 201 | "message": "Сбросить все настройки на значения по умолчанию?" 202 | }, 203 | "resetButtonLabel": { 204 | "message": "Сброс всех данных расширения (в экстренном случае)" 205 | }, 206 | "streamlink": { 207 | "message": "Команда для Streamlink" 208 | }, 209 | "streamlinkOutput": { 210 | "message": "Вывод Streamlink в" 211 | }, 212 | "subtitlePref": { 213 | "message": "Игнорировать субтитры" 214 | }, 215 | "tabAll": { 216 | "message": "Текущая сессия" 217 | }, 218 | "tabPrevious": { 219 | "message": "Предыдущие сессии" 220 | }, 221 | "tabThis": { 222 | "message": "Текущая вкладка" 223 | }, 224 | "tableForm": { 225 | "message": "Запись таблицы" 226 | }, 227 | "timestampPref": { 228 | "message": "Добавлять дату/время к имени файла" 229 | }, 230 | "tipHint": { 231 | "message": "Наведите курсор мыши на вопросительные знаки, чтобы узнать больше о конкретных параметрах." 232 | }, 233 | "titlePref": { 234 | "message": "Показывать заголовок вкладки как источник" 235 | }, 236 | "url": { 237 | "message": "Обычная ссылка" 238 | }, 239 | "user1": { 240 | "message": "Пользовательская команда 1" 241 | }, 242 | "user2": { 243 | "message": "Пользовательская команда 2" 244 | }, 245 | "user3": { 246 | "message": "Пользовательская команда 3" 247 | }, 248 | "userCommand": { 249 | "message": "Пользовательские команды" 250 | }, 251 | "userCommandTip": { 252 | "message": "Доступные переменные:\n%url%\n%filename%\n%useragent%\n%referer%\n%cookie%\n%proxy%\n%origin%\n%tabtitle%\n%timestamp%" 253 | }, 254 | "ytdlp": { 255 | "message": "Команда для yt-dlp" 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/_locales/sk/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "autoDownloadPref": { 3 | "message": "Automaticky sťahovať súbory mimo manifestu" 4 | }, 5 | "autoDownloadTip": { 6 | "message": "Uloží súboru do predvoleného priečinka pre sťahovania. Užitočné napríklad pri sťahovaní streamu počas sledovania. Nepridáva položky do zoznamu adries a nezobrazuje upozornenia, aby používateľ nebol rušený. Na zabránenie nežiadúcich sťahovaní použite zoznam zakázaných adries URL." 7 | }, 8 | "blacklistPref": { 9 | "message": "Použiť zoznam zakázaných adries URL" 10 | }, 11 | "blacklistTip": { 12 | "message": "Jedna položka na riadok, na veľkosti písmen nezáleží. Aplikuje sa na typy súborov a adresy streamov, t. j. doplnok bude prakticky vypnutý pre všetky stránky a streamy/typy súborov, ktoré obsahujú nasledujúci text." 13 | }, 14 | "buttonClick": { 15 | "message": "Funkcia akčného tlačidla prehliadača" 16 | }, 17 | "clearList": { 18 | "message": "Vyčistiť zoznam adries" 19 | }, 20 | "copyAll": { 21 | "message": "Kopírovať viditeľné adresy" 22 | }, 23 | "copyMethod": { 24 | "message": "Kopírovať adresu streamu ako" 25 | }, 26 | "customCommandPref": { 27 | "message": "Dodatočné parametre príkazu" 28 | }, 29 | "customCtPref": { 30 | "message": "Detegovať ďalšie hlavičky Content-Type" 31 | }, 32 | "customCtTip": { 33 | "message": "Jedna položka na riadok. Na veľkosti písmen nezáleží." 34 | }, 35 | "customExtPref": { 36 | "message": "Detegovať ďalšie prípony súborov" 37 | }, 38 | "customExtTip": { 39 | "message": "Jedna položka na riadok." 40 | }, 41 | "deleteTooltip": { 42 | "message": "Kliknutím odstránite adresu URL" 43 | }, 44 | "disablePref": { 45 | "message": "Vypnúť detekciu" 46 | }, 47 | "downloadDirectPref": { 48 | "message": "Sťahovať súbory mimo manifestu miesto kopírovania ich adries URL" 49 | }, 50 | "downloaderPref": { 51 | "message": "Použiť externý sťahovač pre yt-dlp" 52 | }, 53 | "exportButton": { 54 | "message": "Export" 55 | }, 56 | "exportSettings": { 57 | "message": "Exportovať/importovať nastavenia do/zo súboru JSON" 58 | }, 59 | "extText": { 60 | "message": "Deteguje playlisty a titulky vo formáte HLS/DASH/HDS/MSS rovnako ako aj vlastné prípony súborov a hlavičky Content-Type. Zostaví hotové príkazy pre yt-dlp/FFmpeg/Streamlink/N_m3u8DL-RE." 61 | }, 62 | "ffmpeg": { 63 | "message": "príkaz FFmpeg" 64 | }, 65 | "file": { 66 | "message": "Súboru" 67 | }, 68 | "fileExtension": { 69 | "message": "Prípona súboru" 70 | }, 71 | "filePref": { 72 | "message": "Ignorovať priame odkazy na multimediálne súbory" 73 | }, 74 | "fileSizePref": { 75 | "message": "Ignorovať multimediálne súbory menšie ako" 76 | }, 77 | "filenamePref": { 78 | "message": "Súbor pomenovať podľa názvu karty" 79 | }, 80 | "filterInput": { 81 | "message": "Filter" 82 | }, 83 | "headersPref": { 84 | "message": "Pridať dodatočné hlavičky" 85 | }, 86 | "hlsdl": { 87 | "message": "príkaz hlsdl" 88 | }, 89 | "importButton": { 90 | "message": "Import" 91 | }, 92 | "importButtonFailure": { 93 | "message": "Nepodarilo sa importovať nastavenia." 94 | }, 95 | "kodiUrl": { 96 | "message": "adresu URL pre Kodi" 97 | }, 98 | "manifestPref": { 99 | "message": "Ignorovať streamy" 100 | }, 101 | "manifestTip": { 102 | "message": "Toto je hlavné poslanie tohto doplnku. Použite, ak chcete napríklad stiahnuť súbory iba priamo alebo použiť vlastné prípony/typy obsahu súborov." 103 | }, 104 | "multithreadPref": { 105 | "message": "Sťahovať súčasne viaceré fragmenty pomocou yt-dlp" 106 | }, 107 | "nm3u8dl": { 108 | "message": "príkaz N_m3u8DL-RE" 109 | }, 110 | "noRestorePref": { 111 | "message": "Vypne funkciu zapamätania predchádzajúcich sedení (vyčistí položky pri spustení)" 112 | }, 113 | "notifCopiedText": { 114 | "message": "Adresa(y) URL bola(i) úspešne skopírovaná(é):\n" 115 | }, 116 | "notifCopiedTitle": { 117 | "message": "Adresa(y) URL skopírovaná(é)!" 118 | }, 119 | "notifDetectPref": { 120 | "message": "Vypnúť upozornenia o detekcii" 121 | }, 122 | "notifDownErrorText": { 123 | "message": "Nepodarilo sa stiahnuť adresu URL:\n" 124 | }, 125 | "notifDownErrorTitle": { 126 | "message": "Chyba pri sťahovaní!" 127 | }, 128 | "notifErrorText": { 129 | "message": "Nebolo možné skopírovať adresu(y) URL:\n" 130 | }, 131 | "notifErrorTitle": { 132 | "message": "Chyba pri kopírovaní!" 133 | }, 134 | "notifIncompCopiedText": { 135 | "message": "Adresa URL skopírovaná(é) bez formátovacích znakov kvôli kompatibilite s vybraným nástrojom:\n" 136 | }, 137 | "notifManyText": { 138 | "message": "Nájdené nové streamy:\n" 139 | }, 140 | "notifManyTitle": { 141 | "message": "Našlo sa viacero streamov!" 142 | }, 143 | "notifPref": { 144 | "message": "Vypnúť všetky notifikácie" 145 | }, 146 | "notifText": { 147 | "message": "Našiel sa nový stream $1:\n" 148 | }, 149 | "notifTitle": { 150 | "message": "Našiel sa stream" 151 | }, 152 | "openOptions": { 153 | "message": "Možnosti" 154 | }, 155 | "openPopup": { 156 | "message": "Otvoriť okno doplnku" 157 | }, 158 | "openSidebar": { 159 | "message": "Otvoriť postrannú lištu doplnku" 160 | }, 161 | "placeholderCell": { 162 | "message": "Nie sú dostupné žiadne adresy URL." 163 | }, 164 | "player": { 165 | "message": "Predvoleného prehrávača" 166 | }, 167 | "popupFilename": { 168 | "message": "Názov súboru" 169 | }, 170 | "popupSize": { 171 | "message": "Veľkosť" 172 | }, 173 | "popupSource": { 174 | "message": "Zdroj" 175 | }, 176 | "popupTimestamp": { 177 | "message": "Časová značka" 178 | }, 179 | "popupType": { 180 | "message": "Typ" 181 | }, 182 | "proxyPref": { 183 | "message": "Použiť HTTP server proxy" 184 | }, 185 | "recentPref": { 186 | "message": "Zobraziť iba určitý počet posledných položiek" 187 | }, 188 | "regexCommandPref": { 189 | "message": "Použiť reg. výraz v používateľom definovanom príkaze" 190 | }, 191 | "regexCommandTip": { 192 | "message": "Zadajte ako reťazec, napr. ab+c. Vykoná nahradenie vo výslednom príkaze. Prázdne pole nahradzovania odstráni zodpovedajúci text." 193 | }, 194 | "regexWarning": { 195 | "message": "Neplatný regulárny výraz." 196 | }, 197 | "resetButton": { 198 | "message": "Obnoviť nastavenia" 199 | }, 200 | "resetButtonConfirm": { 201 | "message": "Naozaj chcete nastaviť všetky nastavenia na predvolené hodnoty?" 202 | }, 203 | "resetButtonLabel": { 204 | "message": "Obnoviť všetky nastavenia (v prípade nutnosti)" 205 | }, 206 | "streamlink": { 207 | "message": "príkaz Streamlink" 208 | }, 209 | "streamlinkOutput": { 210 | "message": "poslať Streamlink do" 211 | }, 212 | "subtitlePref": { 213 | "message": "Ignorovať titulky" 214 | }, 215 | "tabAll": { 216 | "message": "všetkých kariet" 217 | }, 218 | "tabPrevious": { 219 | "message": "minulých sedení" 220 | }, 221 | "tabThis": { 222 | "message": "tejto karty" 223 | }, 224 | "tableForm": { 225 | "message": "položku tabuľky" 226 | }, 227 | "timestampPref": { 228 | "message": "Pridať časovú značku k názvu súboru" 229 | }, 230 | "tipHint": { 231 | "message": "Podržte kurzor myši nad otáznikom a o konkrétnej funkcii tak zistíte viac." 232 | }, 233 | "titlePref": { 234 | "message": "Zobrazovať v mene karty zdroj" 235 | }, 236 | "url": { 237 | "message": "adresu URL" 238 | }, 239 | "user1": { 240 | "message": "používateľom definovaný príkaz 1" 241 | }, 242 | "user2": { 243 | "message": "používateľom definovaný príkaz 2" 244 | }, 245 | "user3": { 246 | "message": "používateľom definovaný príkaz 3" 247 | }, 248 | "userCommand": { 249 | "message": "Používateľom definovaný príkaz" 250 | }, 251 | "userCommandTip": { 252 | "message": "Dostupné polia:\n%url%\n%filename%\n%useragent%\n%referer%\n%cookie%\n%proxy%\n%origin%\n%tabtitle%\n%timestamp%" 253 | }, 254 | "ytdlp": { 255 | "message": "príkaz yt-dlp" 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/css/options.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 3 | Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", Arial, sans-serif; 4 | background: #fff; 5 | color: #202023; 6 | user-select: none; 7 | } 8 | 9 | table { 10 | margin-left: auto; 11 | margin-right: auto; 12 | } 13 | 14 | .tipContainer { 15 | display: inline; 16 | cursor: help; 17 | } 18 | 19 | .tip, 20 | .warning { 21 | font-weight: bold; 22 | margin: auto 1vw; 23 | } 24 | 25 | .warning { 26 | color: #f00; 27 | display: none; 28 | } 29 | 30 | #versionContainer { 31 | padding-top: 2em; 32 | } 33 | 34 | #exportButton { 35 | margin-right: 0.5em; 36 | } 37 | 38 | @media (prefers-color-scheme: dark) { 39 | body { 40 | background: #202023; 41 | color: #fff; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/css/popup.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0 auto; 5 | width: max-content; 6 | max-width: 800px; 7 | max-height: 600px; 8 | } 9 | 10 | body { 11 | display: flex; 12 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 13 | Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", Arial, sans-serif; 14 | font-size: 0.8em; 15 | background: #fff; 16 | color: #202023; 17 | user-select: none; 18 | } 19 | 20 | #container { 21 | display: flex; 22 | flex-direction: column; 23 | min-height: 0; 24 | } 25 | 26 | #urlList { 27 | overflow: auto; 28 | } 29 | 30 | table { 31 | table-layout: fixed; 32 | border-collapse: collapse; 33 | width: 100%; 34 | user-select: text; 35 | } 36 | 37 | table, 38 | th, 39 | td { 40 | padding: 0.75em; 41 | text-align: center; 42 | overflow: hidden; 43 | text-overflow: ellipsis; 44 | white-space: nowrap; 45 | } 46 | 47 | div { 48 | text-align: center; 49 | } 50 | 51 | td *, 52 | th * { 53 | margin: auto 0; 54 | } 55 | 56 | .urlEntry, 57 | #urlList { 58 | border: 1px solid gainsboro; 59 | border-width: 1px 0; 60 | } 61 | 62 | .urlEntry:hover, 63 | .urlEntry:focus-within { 64 | background-color: gainsboro; 65 | } 66 | 67 | .urlEntry:last-of-type { 68 | border-bottom-width: 0; 69 | } 70 | 71 | .urlCell { 72 | white-space: initial; 73 | word-break: break-all; 74 | cursor: pointer; 75 | text-overflow: initial; 76 | } 77 | 78 | input, 79 | select { 80 | margin: auto 0.25em; 81 | } 82 | 83 | input, 84 | select, 85 | label { 86 | vertical-align: middle; 87 | } 88 | 89 | a { 90 | color: inherit; 91 | } 92 | 93 | #clearFilterInput { 94 | text-decoration: none; 95 | } 96 | 97 | @media (prefers-color-scheme: dark) { 98 | body { 99 | background: #202023; 100 | color: #fff; 101 | } 102 | 103 | .urlEntry:hover, 104 | .urlEntry:focus-within { 105 | color: #232323; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/css/sidebar.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 3 | Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", Arial, sans-serif; 4 | background: #fff; 5 | color: #202023; 6 | user-select: none; 7 | margin: 0; 8 | padding: 0.5em; 9 | } 10 | 11 | #urlList { 12 | overflow: auto; 13 | } 14 | 15 | table { 16 | table-layout: fixed; 17 | border-collapse: collapse; 18 | width: 100%; 19 | user-select: text; 20 | } 21 | 22 | table, 23 | th, 24 | td { 25 | padding: 0.33em 0; 26 | text-align: center; 27 | overflow: hidden; 28 | text-overflow: ellipsis; 29 | white-space: nowrap; 30 | } 31 | 32 | div { 33 | text-align: center; 34 | } 35 | 36 | td *, 37 | th * { 38 | margin: auto 0; 39 | } 40 | 41 | td { 42 | display: block; 43 | } 44 | 45 | .urlEntry, 46 | #urlList { 47 | border: 1px solid gainsboro; 48 | border-width: 1px 0; 49 | } 50 | 51 | .urlEntry:hover, 52 | .urlEntry:focus-within { 53 | background-color: gainsboro; 54 | } 55 | 56 | .urlEntry:last-of-type { 57 | border-bottom-width: 0; 58 | } 59 | 60 | .urlCell { 61 | white-space: initial; 62 | word-break: break-all; 63 | cursor: pointer; 64 | text-overflow: initial; 65 | } 66 | 67 | .urlEntry > td { 68 | display: table-cell; 69 | } 70 | 71 | input, 72 | select { 73 | margin: auto 0.25em; 74 | } 75 | 76 | input, 77 | select, 78 | label { 79 | vertical-align: middle; 80 | } 81 | 82 | a { 83 | color: inherit; 84 | } 85 | 86 | #clearFilterInput { 87 | text-decoration: none; 88 | } 89 | 90 | @media (prefers-color-scheme: dark) { 91 | body { 92 | background: #202023; 93 | color: #fff; 94 | } 95 | 96 | .urlEntry:hover, 97 | .urlEntry:focus-within { 98 | color: #232323; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/54ac/stream-detector/7f0ba952e0f6051b1337a291ce389bdce2ceffa1/src/favicon.ico -------------------------------------------------------------------------------- /src/img/icon-dark-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/54ac/stream-detector/7f0ba952e0f6051b1337a291ce389bdce2ceffa1/src/img/icon-dark-16.png -------------------------------------------------------------------------------- /src/img/icon-dark-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/54ac/stream-detector/7f0ba952e0f6051b1337a291ce389bdce2ceffa1/src/img/icon-dark-48.png -------------------------------------------------------------------------------- /src/img/icon-dark-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/54ac/stream-detector/7f0ba952e0f6051b1337a291ce389bdce2ceffa1/src/img/icon-dark-96.png -------------------------------------------------------------------------------- /src/img/icon-dark-enabled-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/54ac/stream-detector/7f0ba952e0f6051b1337a291ce389bdce2ceffa1/src/img/icon-dark-enabled-16.png -------------------------------------------------------------------------------- /src/img/icon-dark-enabled-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/54ac/stream-detector/7f0ba952e0f6051b1337a291ce389bdce2ceffa1/src/img/icon-dark-enabled-48.png -------------------------------------------------------------------------------- /src/img/icon-dark-enabled-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/54ac/stream-detector/7f0ba952e0f6051b1337a291ce389bdce2ceffa1/src/img/icon-dark-enabled-96.png -------------------------------------------------------------------------------- /src/img/icon-light-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/54ac/stream-detector/7f0ba952e0f6051b1337a291ce389bdce2ceffa1/src/img/icon-light-16.png -------------------------------------------------------------------------------- /src/img/icon-light-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/54ac/stream-detector/7f0ba952e0f6051b1337a291ce389bdce2ceffa1/src/img/icon-light-48.png -------------------------------------------------------------------------------- /src/img/icon-light-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/54ac/stream-detector/7f0ba952e0f6051b1337a291ce389bdce2ceffa1/src/img/icon-light-96.png -------------------------------------------------------------------------------- /src/img/icon-light-enabled-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/54ac/stream-detector/7f0ba952e0f6051b1337a291ce389bdce2ceffa1/src/img/icon-light-enabled-16.png -------------------------------------------------------------------------------- /src/img/icon-light-enabled-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/54ac/stream-detector/7f0ba952e0f6051b1337a291ce389bdce2ceffa1/src/img/icon-light-enabled-48.png -------------------------------------------------------------------------------- /src/img/icon-light-enabled-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/54ac/stream-detector/7f0ba952e0f6051b1337a291ce389bdce2ceffa1/src/img/icon-light-enabled-96.png -------------------------------------------------------------------------------- /src/js/background.js: -------------------------------------------------------------------------------- 1 | import defaults from "./components/defaults.js"; 2 | import supported from "./components/supported.js"; 3 | import { getStorage, setStorage, clearStorage } from "./components/storage.js"; 4 | 5 | import iconLight16 from "../img/icon-light-16.png"; 6 | import iconLight48 from "../img/icon-light-48.png"; 7 | import iconLight96 from "../img/icon-light-96.png"; 8 | import iconDark16 from "../img/icon-dark-16.png"; 9 | import iconDark48 from "../img/icon-dark-48.png"; 10 | import iconDark96 from "../img/icon-dark-96.png"; 11 | import iconLightEnabled16 from "../img/icon-light-enabled-16.png"; 12 | import iconLightEnabled48 from "../img/icon-light-enabled-48.png"; 13 | import iconLightEnabled96 from "../img/icon-light-enabled-96.png"; 14 | import iconDarkEnabled16 from "../img/icon-dark-enabled-16.png"; 15 | import iconDarkEnabled48 from "../img/icon-dark-enabled-48.png"; 16 | import iconDarkEnabled96 from "../img/icon-dark-enabled-96.png"; 17 | 18 | // firefox/chrome 19 | chrome.browserAction = chrome.browserAction || chrome.action; 20 | 21 | const _ = chrome.i18n.getMessage; 22 | 23 | const CLEAR_STORAGE = false; 24 | 25 | const queue = []; 26 | const allRequestDetails = []; 27 | let urlStorage = []; 28 | let urlStorageRestore = []; 29 | let requestTimeoutId = -1; 30 | 31 | let subtitlePref; 32 | let filePref; 33 | let fileSizePref; 34 | let fileSizeAmount; 35 | let manifestPref; 36 | let blacklistPref; 37 | let blacklistEntries; 38 | let customExtPref; 39 | let customCtPref; 40 | let noRestorePref; 41 | let disablePref; 42 | let notifDetectPref; 43 | let notifPref; 44 | let downloadDirectPref; 45 | let autoDownloadPref; 46 | let newline; 47 | 48 | const customSupported = { ext: [], ct: [], type: "CUSTOM", category: "custom" }; 49 | const isChrome = chrome.runtime.getURL("").startsWith("chrome-extension://"); 50 | const isDarkMode = () => 51 | window.matchMedia("(prefers-color-scheme: dark)").matches; 52 | 53 | const updateVars = async () => { 54 | // the web storage api crashes the entire browser sometimes so I have to resort to this nonsense 55 | subtitlePref = await getStorage("subtitlePref"); 56 | filePref = await getStorage("filePref"); 57 | fileSizePref = await getStorage("fileSizePref"); 58 | fileSizeAmount = await getStorage("fileSizeAmount"); 59 | manifestPref = await getStorage("manifestPref"); 60 | blacklistPref = await getStorage("blacklistPref"); 61 | blacklistEntries = await getStorage("blacklistEntries"); 62 | customExtPref = await getStorage("customExtPref"); 63 | customSupported.ext = await getStorage("customExtEntries"); 64 | customCtPref = await getStorage("customCtPref"); 65 | customSupported.ct = await getStorage("customCtEntries"); 66 | noRestorePref = await getStorage("noRestorePref"); 67 | disablePref = await getStorage("disablePref"); 68 | notifDetectPref = await getStorage("notifDetectPref"); 69 | notifPref = await getStorage("notifPref"); 70 | downloadDirectPref = await getStorage("downloadDirectPref"); 71 | autoDownloadPref = await getStorage("autoDownloadPref"); 72 | }; 73 | 74 | const updateIcons = () => { 75 | if (disablePref !== true) 76 | chrome.browserAction.setIcon({ 77 | path: { 78 | 16: isDarkMode ? iconDarkEnabled16 : iconLightEnabled16, 79 | 48: isDarkMode ? iconDarkEnabled48 : iconLightEnabled48, 80 | 96: isDarkMode ? iconDarkEnabled96 : iconLightEnabled96 81 | } 82 | }); 83 | else 84 | chrome.browserAction.setIcon({ 85 | path: { 86 | 16: isDarkMode ? iconDark16 : iconLight16, 87 | 48: isDarkMode ? iconDark48 : iconLight48, 88 | 96: isDarkMode ? iconDark96 : iconLight96 89 | } 90 | }); 91 | }; 92 | 93 | const addListeners = () => { 94 | chrome.webRequest.onBeforeSendHeaders.addListener( 95 | urlFilter, 96 | { urls: [""] }, 97 | isChrome ? ["requestHeaders", "extraHeaders"] : ["requestHeaders"] 98 | ); 99 | 100 | chrome.webRequest.onHeadersReceived.addListener( 101 | urlFilter, 102 | { urls: [""] }, 103 | isChrome ? ["responseHeaders", "extraHeaders"] : ["responseHeaders"] 104 | ); 105 | }; 106 | 107 | const init = async () => { 108 | for (const option in defaults) { 109 | if ((await getStorage(option)) === null) 110 | // write defaults to storage 111 | await setStorage({ [option]: defaults[option] }); 112 | } 113 | 114 | // reset filter on startup 115 | await setStorage({ filterInput: "" }); 116 | 117 | setStorage({ version: chrome.runtime.getManifest().version }); 118 | 119 | // newline shouldn't really be an issue but just in case 120 | chrome.runtime.getPlatformInfo(async (info) => { 121 | newline = info.os === "win" ? "\r\n" : "\n"; 122 | setStorage({ newline }); 123 | }); 124 | 125 | chrome.browserAction.setBadgeBackgroundColor({ color: "green" }); 126 | chrome.browserAction.setBadgeText({ text: "" }); 127 | 128 | chrome.browserAction.onClicked.addListener( 129 | (tab, OnClickData) => 130 | OnClickData?.button === 1 && chrome.tabs.create({ url: "/popup.html" }) 131 | ); 132 | 133 | await updateVars(); 134 | }; 135 | 136 | const getTabData = async (tab) => 137 | new Promise((resolve) => chrome.tabs.get(tab, (data) => resolve(data))); 138 | 139 | const urlValidator = (e, requestDetails, headerSize, headerCt) => { 140 | if (!e) return false; 141 | 142 | if (requestDetails.tabId === -1) return false; 143 | 144 | const isExistingUrl = urlStorage.find((u) => u.url === requestDetails.url); 145 | if ( 146 | isExistingUrl && 147 | (isExistingUrl.requestId !== requestDetails.requestId || 148 | !queue.includes(requestDetails.requestId)) 149 | ) 150 | return false; 151 | 152 | if (subtitlePref && e.category === "subtitles") return false; 153 | 154 | if (filePref && e.category === "files") return false; 155 | 156 | if ( 157 | fileSizePref && 158 | (e.category === "files" || e.category === "custom") && 159 | headerSize && 160 | Math.floor(headerSize.value / 1024 / 1024) < Number(fileSizeAmount) 161 | ) 162 | return false; 163 | 164 | if (manifestPref && e.category === "stream") return false; 165 | 166 | if ( 167 | blacklistPref && 168 | blacklistEntries?.some( 169 | (entry) => 170 | requestDetails.url.toLowerCase().includes(entry.toLowerCase()) || 171 | ( 172 | requestDetails.documentUrl || 173 | requestDetails.originUrl || 174 | requestDetails.initiator 175 | ) 176 | ?.toLowerCase() 177 | .includes(entry.toLowerCase()) || 178 | headerCt?.value?.toLowerCase().includes(entry.toLowerCase()) || 179 | e.type.toLowerCase().includes(entry.toLowerCase()) 180 | ) 181 | ) 182 | return false; 183 | 184 | return true; 185 | }; 186 | 187 | const urlFilter = (requestDetails) => { 188 | let ext; 189 | let head; 190 | 191 | const url = new URL(requestDetails.url).pathname.toLowerCase(); 192 | // check file extension and see if the url matches 193 | ext = 194 | customExtPref && 195 | customSupported.ext?.some((fe) => url.toLowerCase().includes("." + fe)) && 196 | customSupported; 197 | if (!ext) 198 | ext = supported.find((f) => 199 | f.ext?.some((fe) => url.toLowerCase().includes("." + fe)) 200 | ); 201 | 202 | // depends which listener caught it 203 | requestDetails.headers = 204 | requestDetails.responseHeaders || requestDetails.requestHeaders; 205 | 206 | const headerCt = requestDetails.headers?.find( 207 | (h) => h.name.toLowerCase() === "content-type" 208 | ); 209 | if (headerCt?.value) { 210 | // check content type header and see if it matches 211 | head = 212 | customCtPref && 213 | customSupported?.ct?.some((fe) => 214 | headerCt.value.toLowerCase().includes(fe.toLowerCase()) 215 | ) && 216 | customSupported; 217 | if (!head) 218 | head = supported.find((f) => 219 | f.ct?.some((fe) => headerCt.value.toLowerCase() === fe.toLowerCase()) 220 | ); 221 | } 222 | 223 | const headerSize = requestDetails.headers?.find( 224 | (h) => h.name.toLowerCase() === "content-length" 225 | ); 226 | 227 | const e = head || ext; 228 | 229 | if (!urlValidator(e, requestDetails, headerSize, headerCt)) return; 230 | queue.push(requestDetails.requestId); 231 | requestDetails.type = e.type; 232 | requestDetails.category = e.category; 233 | addURL(requestDetails); 234 | }; 235 | 236 | const addURL = async (requestDetails) => { 237 | const url = new URL(requestDetails.url); 238 | 239 | // MSS workaround 240 | const urlPath = url.pathname.toLowerCase().includes(".ism/manifest") 241 | ? url.pathname.slice(0, url.pathname.lastIndexOf("/")) 242 | : url.pathname; 243 | 244 | const filename = +urlPath.lastIndexOf("/") 245 | ? urlPath.slice(urlPath.lastIndexOf("/") + 1) 246 | : urlPath[0] === "/" 247 | ? urlPath.slice(1) 248 | : urlPath; 249 | 250 | const { hostname } = url; 251 | 252 | const tabData = await getTabData(requestDetails.tabId); 253 | 254 | // web storage api optimization 255 | const newRequestDetails = { 256 | category: requestDetails.category, 257 | documentUrl: requestDetails.documentUrl, 258 | originUrl: requestDetails.originUrl, 259 | initiator: requestDetails.initiator, 260 | requestId: requestDetails.requestId, 261 | tabId: requestDetails.tabId, 262 | timeStamp: requestDetails.timeStamp, 263 | type: requestDetails.type, 264 | url: requestDetails.url, 265 | headers: requestDetails.headers?.filter( 266 | (h) => 267 | h.name.toLowerCase() === "user-agent" || 268 | h.name.toLowerCase() === "referer" || 269 | h.name.toLowerCase() === "cookie" || 270 | h.name.toLowerCase() === "set-cookie" || 271 | h.name.toLowerCase() === "content-length" 272 | ), 273 | filename, 274 | hostname, 275 | tabData: { 276 | title: tabData?.title, 277 | url: tabData?.url, 278 | incognito: tabData?.incognito 279 | } 280 | }; 281 | 282 | const isExistingRequest = urlStorage.find( 283 | (u) => u.requestId === requestDetails.requestId 284 | ); 285 | if (!isExistingRequest) { 286 | urlStorage.push(newRequestDetails); 287 | chrome.browserAction.getBadgeText({}, (badgeText) => 288 | chrome.browserAction.setBadgeText({ 289 | text: (Number(badgeText) + 1).toString() 290 | }) 291 | ); 292 | } else { 293 | const mergedHeaders = [ 294 | ...isExistingRequest.headers, 295 | ...newRequestDetails.headers 296 | ]; 297 | 298 | urlStorage[ 299 | urlStorage.findIndex((u) => u.requestId === requestDetails.requestId) 300 | ].headers = mergedHeaders; 301 | } 302 | 303 | // debounce lots of requests in a short period of time 304 | clearTimeout(requestTimeoutId); 305 | allRequestDetails.push({ 306 | requestId: newRequestDetails.requestId, 307 | filename: newRequestDetails.filename, 308 | type: newRequestDetails.type 309 | }); 310 | 311 | requestTimeoutId = setTimeout(async () => { 312 | await setStorage({ urlStorage }); 313 | chrome.runtime.sendMessage({ urlStorage: true }); // update popup if opened 314 | 315 | allRequestDetails 316 | .map((d) => d.requestId) 317 | .forEach((id) => queue.splice(queue.indexOf(id, 1))); // remove all batched requests from queue 318 | 319 | if ( 320 | !notifDetectPref && 321 | !notifPref && 322 | (!autoDownloadPref || (autoDownloadPref && filePref)) 323 | ) { 324 | if (allRequestDetails.length > 1) 325 | // multiple files detected 326 | chrome.notifications.create("add", { 327 | // id = only one notification of this type appears at a time 328 | type: "basic", 329 | iconUrl: iconDark96, 330 | title: _("notifManyTitle"), 331 | message: 332 | _("notifManyText") + 333 | allRequestDetails.map((d) => d.filename).join(newline) 334 | }); 335 | else 336 | chrome.notifications.create("add", { 337 | type: "basic", 338 | iconUrl: iconDark96, 339 | title: _("notifTitle"), 340 | message: _("notifText", requestDetails.type) + filename 341 | }); 342 | } 343 | 344 | allRequestDetails.length = 0; // clear array for next batch 345 | }, 100); 346 | 347 | // auto download file 348 | if ( 349 | (newRequestDetails.category === "files" || 350 | newRequestDetails.category === "custom") && 351 | downloadDirectPref && 352 | autoDownloadPref 353 | ) { 354 | const dlOptions = chrome.runtime 355 | .getURL("") 356 | .startsWith("chrome-extension://") 357 | ? { 358 | filename: newRequestDetails.filename, 359 | url: newRequestDetails.url, 360 | saveAs: false 361 | } 362 | : { 363 | filename: newRequestDetails.filename, 364 | headers: 365 | newRequestDetails.headers?.filter( 366 | (h) => h.name.toLowerCase() === "referer" 367 | ) || [], 368 | incognito: newRequestDetails.tabData?.incognito || false, 369 | url: newRequestDetails.url, 370 | saveAs: false 371 | }; 372 | 373 | chrome.downloads.download(dlOptions); 374 | } 375 | }; 376 | 377 | const deleteURL = async (message) => { 378 | // url deletion 379 | if (message.previous !== true) { 380 | urlStorage = urlStorage.filter( 381 | (url) => 382 | !message.delete 383 | .map((msgUrl) => msgUrl.requestId) 384 | .includes(url.requestId) 385 | ); 386 | } else { 387 | urlStorageRestore = urlStorageRestore.filter( 388 | (url) => 389 | !message.delete 390 | .map((msgUrl) => msgUrl.requestId) 391 | .includes(url.requestId) 392 | ); 393 | } 394 | 395 | await setStorage({ urlStorage }); 396 | await setStorage({ urlStorageRestore }); 397 | chrome.runtime.sendMessage({ urlStorage: true }); 398 | }; 399 | 400 | (async () => { 401 | // clear everything and/or set up 402 | 403 | // cleanup for major updates 404 | const manifestVersion = chrome.runtime.getManifest().version; 405 | const addonVersion = await getStorage("version"); 406 | if (CLEAR_STORAGE && addonVersion && addonVersion !== manifestVersion) 407 | await clearStorage(); 408 | //specifically for v2.11.2 409 | if ( 410 | addonVersion && 411 | addonVersion !== manifestVersion && 412 | (await getStorage("noRestorePref")) 413 | ) 414 | await setStorage({ noRestorePref: false }); 415 | 416 | await init(); 417 | 418 | if (disablePref !== true) { 419 | addListeners(); 420 | updateIcons(); 421 | } 422 | 423 | urlStorage = await getStorage("urlStorage"); 424 | urlStorageRestore = await getStorage("urlStorageRestore"); 425 | 426 | // restore urls on startup 427 | if (urlStorage && urlStorage.length > 0 && !noRestorePref) { 428 | urlStorageRestore = [...urlStorageRestore, ...urlStorage]; 429 | 430 | // remove all entries previously detected in private windows 431 | urlStorageRestore = urlStorageRestore.filter( 432 | (url) => url.tabData?.incognito !== true 433 | ); 434 | 435 | await setStorage({ urlStorageRestore }); 436 | } else { 437 | urlStorageRestore = []; 438 | await setStorage({ urlStorageRestore }); 439 | } 440 | // urls from previous session were moved to urlStorageRestore 441 | urlStorage = []; 442 | await setStorage({ urlStorage }); 443 | 444 | chrome.runtime.onMessage.addListener(async (message) => { 445 | if (message.delete) deleteURL(message); 446 | else if (message.options) { 447 | await updateVars(); 448 | if ( 449 | disablePref === true && 450 | chrome.webRequest.onBeforeSendHeaders.hasListener(urlFilter) && 451 | chrome.webRequest.onHeadersReceived.hasListener(urlFilter) 452 | ) { 453 | chrome.webRequest.onBeforeSendHeaders.removeListener(urlFilter); 454 | chrome.webRequest.onHeadersReceived.removeListener(urlFilter); 455 | } else if ( 456 | disablePref !== true && 457 | !chrome.webRequest.onBeforeSendHeaders.hasListener(urlFilter) && 458 | !chrome.webRequest.onHeadersReceived.hasListener(urlFilter) 459 | ) { 460 | addListeners(); 461 | } 462 | updateIcons(); 463 | } else if (message.reset) { 464 | await clearStorage(); 465 | urlStorage = []; 466 | urlStorageRestore = []; 467 | await init(); 468 | chrome.runtime.sendMessage({ options: true }); 469 | } 470 | }); 471 | 472 | chrome.commands.onCommand.addListener((cmd) => { 473 | if (cmd === "open-popup") chrome.browserAction.openPopup(); 474 | if (cmd === "open-sidebar") chrome.sidebarAction.open(); 475 | }); 476 | 477 | // workaround to detect popup close and manage badge text 478 | chrome.runtime.onConnect.addListener((port) => { 479 | if (port.name === "popup") 480 | port.onDisconnect.addListener(() => { 481 | chrome.browserAction.setBadgeBackgroundColor({ color: "green" }); 482 | chrome.browserAction.setBadgeText({ text: "" }); 483 | }); 484 | }); 485 | })(); 486 | -------------------------------------------------------------------------------- /src/js/components/defaults.js: -------------------------------------------------------------------------------- 1 | const defaults = { 2 | disablePref: false, 3 | subtitlePref: false, 4 | filePref: true, 5 | downloadDirectPref: false, 6 | autoDownloadPref: false, 7 | fileSizePref: false, 8 | fileSizeAmount: "1", 9 | manifestPref: false, 10 | copyMethod: "url", 11 | userCommand: "", 12 | regexCommandPref: false, 13 | regexCommand: "", 14 | regexReplace: "", 15 | customExtPref: false, 16 | customExtEntries: [], 17 | customCtPref: false, 18 | customCtEntries: [], 19 | headersPref: true, 20 | titlePref: true, 21 | filenamePref: false, 22 | timestampPref: false, 23 | fileExtension: "ts", 24 | streamlinkOutput: "file", 25 | downloaderPref: false, 26 | multithreadPref: true, 27 | multithreadAmount: "4", 28 | proxyPref: false, 29 | customCommandPref: false, 30 | customCommand: "", 31 | blacklistPref: false, 32 | blacklistEntries: [], 33 | noRestorePref: false, 34 | recentPref: false, 35 | recentAmount: "5", 36 | notifDetectPref: true, 37 | notifPref: false, 38 | urlStorageRestore: [], 39 | urlStorage: [], 40 | tabThis: true 41 | }; 42 | 43 | export default defaults; 44 | -------------------------------------------------------------------------------- /src/js/components/storage.js: -------------------------------------------------------------------------------- 1 | export const getStorage = async (key) => 2 | new Promise((resolve) => 3 | chrome.storage.local.get(key, (value) => { 4 | if (Object.prototype.hasOwnProperty.call(value, key)) resolve(value[key]); 5 | else resolve(null); 6 | }) 7 | ); 8 | 9 | export const getAllStorage = async () => 10 | new Promise((resolve) => 11 | chrome.storage.local.get(null, (res) => { 12 | if (Object.keys(res)?.length > 0) resolve(res); 13 | else resolve(null); 14 | }) 15 | ); 16 | 17 | export const setStorage = async (obj) => 18 | new Promise((resolve) => chrome.storage.local.set(obj, () => resolve())); 19 | 20 | const removeStorage = async (key) => 21 | new Promise((resolve) => chrome.storage.local.remove(key, () => resolve())); 22 | 23 | export const clearStorage = async () => 24 | new Promise((resolve) => chrome.storage.local.clear(() => resolve())); 25 | 26 | export const saveOptionStorage = async (e, options) => { 27 | if ( 28 | e.target.id === "copyMethod" && 29 | e.target.value !== "url" && 30 | e.target.value !== "tableForm" && 31 | e.target.value !== "kodiUrl" && 32 | !e.target.value.startsWith("user") 33 | ) { 34 | const prefName = "customCommand" + e.target.value; 35 | 36 | if (await getStorage(prefName)) 37 | document.getElementById("customCommand").value = 38 | (await getStorage(prefName)) || ""; 39 | } 40 | 41 | if (e.target.id === "regexCommand") 42 | await setStorage({ [e.target.id]: e.target.value }); 43 | else if (e.target.id === "customCommand") 44 | await setStorage({ 45 | [e.target.id + document.getElementById("copyMethod").value]: 46 | e.target.value?.trim() 47 | }); 48 | else if (e.target.tagName.toLowerCase() === "textarea") 49 | await setStorage({ 50 | [e.target.id]: e.target.value?.split("\n").filter((ee) => ee) 51 | }); 52 | else if (e.target.type === "checkbox") 53 | await setStorage({ [e.target.id]: e.target.checked }); 54 | else if (e.target.type === "text" && e.target.value?.trim().length === 0) 55 | await removeStorage(e.target.id); 56 | else if (e.target.type === "radio" && options.length > 0) { 57 | // update entire radio group 58 | for (const option of options) { 59 | if (option.name === e.target.name) { 60 | await setStorage({ 61 | [option.id]: document.getElementById(option.id).checked 62 | }); 63 | } 64 | } 65 | } else await setStorage({ [e.target.id]: e.target.value?.trim() }); 66 | 67 | // update other scripts as well 68 | chrome.runtime.sendMessage({ options: true }); 69 | }; 70 | -------------------------------------------------------------------------------- /src/js/components/supported.js: -------------------------------------------------------------------------------- 1 | const supported = [ 2 | { 3 | ext: ["m3u8"], 4 | ct: [ 5 | "application/x-mpegurl", 6 | "application/vnd.apple.mpegurl", 7 | "audio/vnd.apple.mpegurl" 8 | ], 9 | type: "HLS", 10 | category: "stream" 11 | }, 12 | { 13 | ext: ["mpd", "json?base64_init=1"], 14 | ct: ["application/dash+xml"], 15 | type: "DASH", 16 | category: "stream" 17 | }, 18 | { ext: ["f4m"], ct: ["application/f4m"], type: "HDS", category: "stream" }, 19 | { ext: ["ism/manifest"], ct: [], type: "MSS", category: "stream" }, 20 | { ext: ["vtt"], ct: ["text/vtt"], type: "VTT", category: "subtitles" }, 21 | { 22 | ext: ["srt"], 23 | ct: ["application/x-subrip"], 24 | type: "SRT", 25 | category: "subtitles" 26 | }, 27 | { 28 | ext: ["ttml", "ttml2"], 29 | ct: ["application/ttml+xml"], 30 | type: "TTML", 31 | category: "subtitles" 32 | }, 33 | { 34 | ext: ["dfxp"], 35 | ct: ["application/ttaf+xml"], 36 | type: "DFXP", 37 | category: "subtitles" 38 | }, 39 | { 40 | ext: ["mp4", "m4v", "m4s"], 41 | ct: ["video/x-m4v", "video/m4v", "video/mp4"], 42 | type: "MP4", 43 | category: "files" 44 | }, 45 | { ext: ["ts", "m2t"], ct: ["video/mp2t"], type: "TS", category: "files" }, 46 | { 47 | ext: ["aac", "m4a"], 48 | ct: ["audio/aac", "audio/m4a"], 49 | type: "AAC", 50 | category: "files" 51 | }, 52 | { ext: ["mp3"], ct: ["audio/mpeg"], type: "MP3", category: "files" }, 53 | { 54 | ext: ["ogg", "ogv", "oga", "opus"], 55 | ct: ["video/ogg", "audio/ogg", "audio/opus"], 56 | type: "OGG", 57 | category: "files" 58 | }, 59 | { 60 | ext: ["weba", "webm"], 61 | ct: ["audio/webm", "video/webm"], 62 | type: "WEBM", 63 | category: "files" 64 | } 65 | ]; 66 | 67 | export default supported; 68 | -------------------------------------------------------------------------------- /src/js/options.js: -------------------------------------------------------------------------------- 1 | import { 2 | saveOptionStorage, 3 | getStorage, 4 | getAllStorage, 5 | setStorage 6 | } from "./components/storage.js"; 7 | 8 | const _ = chrome.i18n.getMessage; // i18n 9 | 10 | const restoreOptions = async () => { 11 | const options = document.getElementsByClassName("option"); 12 | for (const option of options) { 13 | if (option.id === "customCommand") { 14 | const prefName = option.id + document.getElementById("copyMethod").value; 15 | document.getElementById(option.id).value = 16 | (await getStorage(prefName)) || ""; 17 | } else if (option.id === "regexCommand") { 18 | document.getElementById("regexCommand").value = await getStorage( 19 | "regexCommand" 20 | ); 21 | regexValidator(); 22 | } else if (option.tagName.toLowerCase() === "textarea") { 23 | if (await getStorage(option.id)) { 24 | const textareaValue = await getStorage(option.id); 25 | if (textareaValue !== null) 26 | document.getElementById(option.id).value = textareaValue.join("\n"); 27 | } 28 | } else if ((await getStorage(option.id)) !== null) { 29 | if ( 30 | document.getElementById(option.id).type === "checkbox" || 31 | document.getElementById(option.id).type === "radio" 32 | ) { 33 | document.getElementById(option.id).checked = await getStorage( 34 | option.id 35 | ); 36 | } else { 37 | document.getElementById(option.id).value = await getStorage(option.id); 38 | } 39 | } 40 | } 41 | }; 42 | 43 | const regexValidator = () => { 44 | try { 45 | new RegExp(document.getElementById("regexCommand").value); 46 | document.getElementById("regexWarning").style.display = "none"; 47 | } catch { 48 | document.getElementById("regexWarning").style.display = "unset"; 49 | } 50 | }; 51 | 52 | document.addEventListener("DOMContentLoaded", async () => { 53 | const options = document.getElementsByClassName("option"); 54 | for (const option of options) { 55 | if (option.id === "regexCommand") option.oninput = () => regexValidator(); 56 | if (option.type !== "button") option.onchange = (e) => saveOptionStorage(e); 57 | } 58 | 59 | // buttons 60 | document.getElementById("exportButton").onclick = async () => { 61 | const allStorage = await getAllStorage(); 62 | delete allStorage.urlStorage; 63 | delete allStorage.urlStorageRestore; 64 | delete allStorage.version; 65 | delete allStorage.newline; 66 | 67 | const settingsBlob = new Blob([JSON.stringify(allStorage)], { 68 | type: "application/json" 69 | }); 70 | const settingsFile = document.createElement("a"); 71 | settingsFile.href = URL.createObjectURL(settingsBlob); 72 | settingsFile.download = `stream-detector-settings-${Date.now()}.json`; 73 | settingsFile.click(); 74 | URL.revokeObjectURL(settingsBlob); 75 | settingsFile.remove(); 76 | }; 77 | document.getElementById("importButton").onclick = () => { 78 | const settingsFile = document.createElement("input"); 79 | settingsFile.type = "file"; 80 | settingsFile.accept = ".json"; 81 | 82 | settingsFile.onchange = () => { 83 | const settingsReader = new FileReader(); 84 | const [file] = settingsFile.files; 85 | 86 | settingsReader.onload = () => { 87 | try { 88 | JSON.parse(settingsReader.result); 89 | } catch { 90 | window.alert(_("importButtonFailure")); 91 | } 92 | setStorage(JSON.parse(settingsReader.result)); 93 | restoreOptions(); 94 | chrome.runtime.sendMessage({ options: true }); 95 | settingsFile.remove(); 96 | }; 97 | if (file) settingsReader.readAsText(file); 98 | }; 99 | settingsFile.click(); 100 | }; 101 | 102 | document.getElementById("resetButton").onclick = () => 103 | window.confirm(_("resetButtonConfirm")) && 104 | chrome.runtime.sendMessage({ reset: true }); 105 | 106 | restoreOptions(); 107 | 108 | // i18n 109 | const labels = document.getElementsByTagName("label"); 110 | for (const label of labels) { 111 | if (label.htmlFor === "versionTag") 112 | label.textContent = `v${await getStorage("version")}. ${_("tipHint")}`; 113 | else label.textContent = _(label.htmlFor) + ":"; 114 | } 115 | const selectOptions = document.getElementsByTagName("option"); 116 | for (const selectOption of selectOptions) { 117 | if (!selectOption.textContent) 118 | selectOption.textContent = _(selectOption.value); 119 | } 120 | const spans = document.getElementsByTagName("span"); 121 | for (const span of spans) { 122 | // mouseover tooltip 123 | span.parentElement.title = _(span.id); 124 | } 125 | const buttons = document.getElementsByTagName("button"); 126 | for (const button of buttons) { 127 | button.textContent = _(button.id); 128 | } 129 | 130 | // sync with popup changes 131 | chrome.runtime.onMessage.addListener((message) => { 132 | if (message.options) restoreOptions(); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /src/js/popup.js: -------------------------------------------------------------------------------- 1 | import { 2 | saveOptionStorage, 3 | getStorage, 4 | setStorage 5 | } from "./components/storage.js"; 6 | 7 | import notifIcon from "../img/icon-dark-96.png"; 8 | 9 | // firefox/chrome 10 | chrome.browserAction = chrome.browserAction || chrome.action; 11 | const isChrome = chrome.runtime.getURL("").startsWith("chrome-extension://"); 12 | 13 | const _ = chrome.i18n.getMessage; // i18n 14 | 15 | const table = document.getElementById("popupUrlList"); 16 | 17 | let titlePref; 18 | let filenamePref; 19 | let timestampPref; 20 | let downloadDirectPref; 21 | let newline; 22 | let recentPref; 23 | let recentAmount; 24 | let noRestorePref; 25 | let urlList = []; 26 | 27 | const getTimestamp = (timestamp) => { 28 | const date = new Date(timestamp); 29 | return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`; 30 | }; 31 | 32 | // https://stackoverflow.com/a/18650828 33 | const formatBytes = (bytes) => { 34 | const sizes = ["B", "KB", "MB", "GB", "TB"]; 35 | 36 | const i = Math.floor(Math.log(bytes) / Math.log(1024)); 37 | 38 | return parseFloat((bytes / 1024 ** i).toFixed(2)) + " " + sizes[i]; 39 | }; 40 | 41 | const downloadURL = (file) => { 42 | // only firefox supports replacing the referer header 43 | const dlOptions = chrome.runtime.getURL("").startsWith("chrome-extension://") 44 | ? { 45 | url: file.url 46 | } 47 | : { 48 | headers: 49 | file.headers?.filter((h) => h.name.toLowerCase() === "referer") || [], 50 | incognito: file.tabData?.incognito || false, 51 | url: file.url 52 | }; 53 | 54 | chrome.downloads.download( 55 | dlOptions, 56 | (err) => 57 | // returns undefined if download is not successful 58 | err === undefined && 59 | chrome.notifications.create("error", { 60 | type: "basic", 61 | iconUrl: notifIcon, 62 | title: _("notifDownErrorTitle"), 63 | message: _("notifDownErrorText") + file.filename 64 | }) 65 | ); 66 | }; 67 | 68 | const copyURL = async (info) => { 69 | const list = { urls: [], filenames: [], methodIncomp: false }; 70 | for (const e of info) { 71 | let code; 72 | let methodIncomp; 73 | let fileMethod; 74 | 75 | const streamURL = e.url; 76 | const { filename } = e; 77 | fileMethod = (await getStorage("copyMethod")) || "url"; // default to url - just in case 78 | 79 | // don't use user-defined command if empty 80 | if ( 81 | fileMethod.startsWith("user") && 82 | (await getStorage("userCommand" + fileMethod.at(-1))) === null 83 | ) { 84 | fileMethod = "url"; 85 | methodIncomp = true; 86 | } 87 | 88 | if (fileMethod === "url") code = streamURL; 89 | else if (fileMethod === "tableForm") 90 | code = `${streamURL} | ${ 91 | titlePref && e.tabData?.title && !streamURL.includes(e.tabData.title) 92 | ? e.tabData.title 93 | : e.hostname 94 | } | ${getTimestamp(e.timeStamp)}`; 95 | else if (fileMethod === "kodiUrl") code = streamURL; 96 | else if (fileMethod === "ffmpeg") code = "ffmpeg"; 97 | else if (fileMethod === "streamlink") code = "streamlink"; 98 | else if (fileMethod === "ytdlp") { 99 | code = "yt-dlp --no-part --restrict-filenames"; 100 | 101 | if ( 102 | (await getStorage("multithreadPref")) && 103 | (await getStorage("multithreadAmount")) 104 | ) 105 | code += ` -N ${await getStorage("multithreadAmount")}`; 106 | 107 | if ( 108 | (await getStorage("downloaderPref")) && 109 | (await getStorage("downloaderCommand")) 110 | ) 111 | code += ` --downloader "${await getStorage("downloaderCommand")}"`; 112 | } else if (fileMethod === "hlsdl") code = "hlsdl -b -c"; 113 | else if (fileMethod === "nm3u8dl") code = `N_m3u8DL-RE "${streamURL}"`; 114 | else if (fileMethod.startsWith("user")) 115 | code = await getStorage("userCommand" + fileMethod.at(-1)); 116 | 117 | // custom command line 118 | const prefName = `customCommand${fileMethod}`; 119 | if ((await getStorage("customCommandPref")) && (await getStorage(prefName))) 120 | code += ` ${await getStorage(prefName)}`; 121 | 122 | // http proxy 123 | if ((await getStorage("proxyPref")) && (await getStorage("proxyCommand"))) { 124 | if (fileMethod === "ffmpeg") 125 | code += ` -http_proxy "${await getStorage("proxyCommand")}"`; 126 | else if (fileMethod === "streamlink") 127 | code += ` --http-proxy "${await getStorage("proxyCommand")}"`; 128 | else if (fileMethod === "ytdlp") 129 | code += ` --proxy "${await getStorage("proxyCommand")}"`; 130 | else if (fileMethod === "hlsdl") 131 | code += ` -p "${await getStorage("proxyCommand")}"`; 132 | else if (fileMethod === "nm3u8dl") 133 | code += ` --custom-proxy "${await getStorage("proxyCommand")}"`; 134 | else if (fileMethod.startsWith("user")) 135 | code = code.replace( 136 | new RegExp("%proxy%", "g"), 137 | await getStorage("proxyCommand") 138 | ); 139 | } 140 | 141 | // additional headers 142 | if (await getStorage("headersPref")) { 143 | let headerUserAgent = e.headers.find( 144 | (header) => header.name.toLowerCase() === "user-agent" 145 | ); 146 | headerUserAgent 147 | ? (headerUserAgent = headerUserAgent.value) 148 | : (headerUserAgent = navigator.userAgent); 149 | 150 | let headerCookie = e.headers.find( 151 | (header) => 152 | header.name.toLowerCase() === "cookie" || 153 | header.name.toLowerCase() === "set-cookie" 154 | ); 155 | if (headerCookie) 156 | headerCookie = headerCookie.value.replace(new RegExp(`"`, "g"), `'`); // double quotation marks mess up the command 157 | 158 | let headerReferer = e.headers.find( 159 | (header) => header.name.toLowerCase() === "referer" 160 | ); 161 | headerReferer = headerReferer 162 | ? headerReferer.value 163 | : e.originUrl || e.documentUrl || e.initiator || e.tabData?.url; 164 | if ( 165 | headerReferer?.startsWith("about:") || 166 | headerReferer?.startsWith("chrome:") 167 | ) 168 | headerReferer = undefined; 169 | 170 | if (headerUserAgent) { 171 | if (fileMethod === "kodiUrl") 172 | code += `|User-Agent=${encodeURIComponent(headerUserAgent)}`; 173 | else if (fileMethod === "ffmpeg") 174 | code += ` -user_agent "${headerUserAgent}"`; 175 | else if (fileMethod === "streamlink") 176 | code += ` --http-header "User-Agent=${headerUserAgent}"`; 177 | else if (fileMethod === "ytdlp") 178 | code += ` --user-agent "${headerUserAgent}"`; 179 | else if (fileMethod === "hlsdl") code += ` -u "${headerUserAgent}"`; 180 | else if (fileMethod === "nm3u8dl") 181 | code += ` --header "User-Agent: ${headerUserAgent}"`; 182 | else if (fileMethod.startsWith("user")) 183 | code = code.replace(new RegExp("%useragent%", "g"), headerUserAgent); 184 | } else if (fileMethod.startsWith("user")) 185 | code = code.replace(new RegExp("%useragent%", "g"), ""); 186 | 187 | if (headerCookie) { 188 | if (fileMethod === "kodiUrl") { 189 | if (headerUserAgent) code += "&"; 190 | else code += "|"; 191 | code += `Cookie=${encodeURIComponent(headerCookie)}`; 192 | } else if (fileMethod === "ffmpeg") 193 | code += ` -headers "Cookie: ${headerCookie}"`; 194 | else if (fileMethod === "streamlink") 195 | code += ` --http-header "Cookie=${headerCookie}"`; 196 | else if (fileMethod === "ytdlp") 197 | code += ` --add-header "Cookie:${headerCookie}"`; 198 | else if (fileMethod === "hlsdl") code += ` -h "Cookie:${headerCookie}"`; 199 | else if (fileMethod === "nm3u8dl") 200 | code += ` --header "Cookie: ${headerCookie}"`; 201 | else if (fileMethod.startsWith("user")) 202 | code = code.replace(new RegExp("%cookie%", "g"), headerCookie); 203 | } else if (fileMethod === "ytdlp") { 204 | if (!isChrome) code += ` --cookies-from-browser firefox`; 205 | else code += ` --cookies-from-browser chrome`; 206 | } else if (fileMethod.startsWith("user")) 207 | code = code.replace(new RegExp("%cookie%", "g"), ""); 208 | 209 | if (headerReferer) { 210 | if (fileMethod === "kodiUrl") { 211 | if (headerUserAgent || headerCookie) code += "&"; 212 | else code += "|"; 213 | code += `Referer=${encodeURIComponent(headerReferer)}`; 214 | } else if (fileMethod === "ffmpeg") 215 | code += ` -referer "${headerReferer}"`; 216 | else if (fileMethod === "streamlink") 217 | code += ` --http-header "Referer=${headerReferer}"`; 218 | else if (fileMethod === "ytdlp") 219 | code += ` --referer "${headerReferer}"`; 220 | else if (fileMethod === "hlsdl") 221 | code += ` -h "Referer:${headerReferer}"`; 222 | else if (fileMethod === "nm3u8dl") 223 | code += ` --header "Referer: ${headerReferer}"`; 224 | else if (fileMethod.startsWith("user")) 225 | code = code.replace(new RegExp("%referer%", "g"), headerReferer); 226 | } else if (fileMethod.startsWith("user")) 227 | code = code.replace(new RegExp("%referer%", "g"), ""); 228 | } 229 | 230 | if ( 231 | fileMethod.startsWith("user") && 232 | (e.documentUrl || e.originUrl || e.initiator || e.tabData?.url) 233 | ) 234 | code = code.replace( 235 | new RegExp("%origin%", "g"), 236 | e.documentUrl || e.originUrl || e.initiator || e.tabData?.url 237 | ); 238 | else if (fileMethod.startsWith("user")) 239 | code = code.replace(new RegExp("%origin%", "g"), ""); 240 | 241 | if (fileMethod.startsWith("user") && e.tabData?.title) 242 | code = code.replace( 243 | new RegExp("%tabtitle%", "g"), 244 | e.tabData.title.replace(/[/\\?%*:|"<>]/g, "_") 245 | ); 246 | else if (fileMethod.startsWith("user")) 247 | code = code.replace(new RegExp("%tabtitle%", "g"), ""); 248 | 249 | let outFilename; 250 | if (filenamePref && e.tabData?.title) outFilename = e.tabData.title; 251 | else { 252 | outFilename = filename; 253 | if (outFilename.indexOf(".") !== -1) { 254 | // filename without extension 255 | outFilename = outFilename.split("."); 256 | outFilename.pop(); 257 | outFilename = outFilename.join("."); 258 | } 259 | } 260 | 261 | // sanitize tab title and timestamp 262 | outFilename = outFilename.replace(/[/\\?%*:|"<>]/g, "_"); 263 | const outExtension = (await getStorage("fileExtension")) || "ts"; 264 | const outTimestamp = getTimestamp(e.timeStamp).replace( 265 | /[/\\?%*:|"<>]/g, 266 | "_" 267 | ); 268 | 269 | // final part of command 270 | if (fileMethod === "ffmpeg") { 271 | code += ` -i "${streamURL}" -c copy "${outFilename}`; 272 | if (timestampPref) code += ` ${outTimestamp}`; 273 | code += `.${outExtension}"`; 274 | } else if (fileMethod === "streamlink") { 275 | if ((await getStorage("streamlinkOutput")) === "file") { 276 | code += ` -o "${outFilename}`; 277 | if (timestampPref) code += ` ${outTimestamp}`; 278 | code += `.${outExtension}"`; 279 | } 280 | code += ` "${streamURL}" best`; 281 | } else if (fileMethod === "ytdlp") { 282 | if ((filenamePref && e.tabData?.title) || timestampPref) { 283 | code += ` --output "${outFilename}`; 284 | if (timestampPref) code += ` %(epoch)s`; 285 | code += `.%(ext)s"`; 286 | } 287 | code += ` "${streamURL}"`; 288 | } else if (fileMethod === "hlsdl") { 289 | code += ` -o "${outFilename}`; 290 | if (timestampPref) code += ` ${outTimestamp}`; 291 | code += `.${outExtension}" "${streamURL}"`; 292 | } else if (fileMethod === "nm3u8dl") { 293 | code += ` --save-name "${outFilename}`; 294 | if (timestampPref) code += ` ${outTimestamp}`; 295 | code += `"`; 296 | } else if (fileMethod.startsWith("user")) { 297 | code = code.replace(new RegExp("%url%", "g"), streamURL); 298 | code = code.replace(new RegExp("%filename%", "g"), filename); 299 | code = code.replace(new RegExp("%timestamp%", "g"), outTimestamp); 300 | } 301 | 302 | // regex for user command 303 | if ( 304 | fileMethod.startsWith("user") && 305 | (await getStorage("regexCommandPref")) 306 | ) { 307 | const regexCommand = await getStorage("regexCommand"); 308 | const regexReplace = await getStorage("regexReplace"); 309 | 310 | code = code.replace(new RegExp(regexCommand, "g"), regexReplace || ""); 311 | } 312 | 313 | // used to communicate with clipboard/notifications api 314 | list.urls.push(code); 315 | list.filenames.push(filename); 316 | list.methodIncomp = methodIncomp; 317 | } 318 | 319 | try { 320 | if (navigator.clipboard?.writeText) 321 | navigator.clipboard.writeText(list.urls.join(newline)); 322 | else { 323 | // old copying method for compatibility purposes 324 | const copyText = document.createElement("textarea"); 325 | copyText.style.position = "absolute"; 326 | copyText.style.left = "-5454px"; 327 | copyText.style.top = "-5454px"; 328 | document.body.appendChild(copyText); 329 | copyText.value = list.urls.join(newline); 330 | copyText.select(); 331 | document.execCommand("copy"); 332 | document.body.removeChild(copyText); 333 | } 334 | if ((await getStorage("notifPref")) === false) { 335 | chrome.notifications.create("copy", { 336 | type: "basic", 337 | iconUrl: notifIcon, 338 | title: _("notifCopiedTitle"), 339 | message: 340 | (list.methodIncomp 341 | ? _("notifIncompCopiedText") 342 | : _("notifCopiedText")) + list.filenames.join(newline) 343 | }); 344 | } 345 | } catch (e) { 346 | chrome.notifications.create("error", { 347 | type: "basic", 348 | iconUrl: notifIcon, 349 | title: _("notifErrorTitle"), 350 | message: _("notifErrorText") + e 351 | }); 352 | } 353 | }; 354 | 355 | const handleURL = (url) => { 356 | if ( 357 | downloadDirectPref && 358 | (url.category === "files" || url.category === "custom") 359 | ) 360 | downloadURL(url); 361 | else copyURL([url]); 362 | }; 363 | 364 | const deleteURL = (requestDetails) => { 365 | const deleteUrlStorage = [requestDetails]; 366 | chrome.runtime.sendMessage({ 367 | delete: deleteUrlStorage, 368 | previous: document.getElementById("tabPrevious").checked 369 | }); // notify background script to update urlstorage. workaround 370 | }; 371 | 372 | const getIdList = () => 373 | Array.from( 374 | document.getElementById("popupUrlList").getElementsByTagName("tr") 375 | ).map((tr) => tr.id); 376 | 377 | const copyAll = () => { 378 | // this seems like a roundabout way of doing this but oh well 379 | const idList = getIdList(); 380 | const copyUrlList = urlList.filter((url) => idList.includes(url.requestId)); 381 | 382 | copyURL(copyUrlList); 383 | }; 384 | 385 | const clearList = () => { 386 | const idList = getIdList(); 387 | const deleteUrlStorage = urlList.filter((url) => 388 | idList.includes(url.requestId) 389 | ); 390 | 391 | chrome.runtime.sendMessage({ 392 | delete: deleteUrlStorage, 393 | previous: document.getElementById("tabPrevious").checked 394 | }); 395 | }; 396 | 397 | const createList = async () => { 398 | const insertList = (urls) => { 399 | document.getElementById("copyAll").disabled = false; 400 | document.getElementById("clearList").disabled = false; 401 | document.getElementById("filterInput").disabled = false; 402 | document.getElementById("headers").style.display = ""; 403 | 404 | for (const requestDetails of urls) { 405 | // everyone's favorite - dom manipulation in vanilla js 406 | const row = document.createElement("tr"); 407 | row.id = requestDetails.requestId; 408 | row.className = "urlEntry"; 409 | 410 | if (document.body.id === "popup") { 411 | const extCell = document.createElement("td"); 412 | extCell.textContent = 413 | (requestDetails.category === "files" || 414 | requestDetails.category === "custom") && 415 | downloadDirectPref 416 | ? "🔽 " + requestDetails.type.toUpperCase() 417 | : requestDetails.type.toUpperCase(); 418 | row.appendChild(extCell); 419 | } 420 | 421 | const urlCell = document.createElement("td"); 422 | urlCell.className = "urlCell"; 423 | const urlHref = document.createElement("a"); 424 | urlHref.textContent = requestDetails.filename; 425 | urlHref.href = requestDetails.url; 426 | urlCell.onclick = (e) => { 427 | e.preventDefault(); 428 | handleURL(requestDetails); 429 | }; 430 | urlHref.onclick = (e) => { 431 | e.preventDefault(); 432 | e.stopPropagation(); 433 | handleURL(requestDetails); 434 | }; 435 | urlHref.title = requestDetails.url; 436 | urlCell.appendChild(urlHref); 437 | row.appendChild(urlCell); 438 | 439 | if (document.body.id === "popup") { 440 | const sizeCell = document.createElement("td"); 441 | const sizeCellHeader = requestDetails.headers.find( 442 | (header) => header.name.toLowerCase() === "content-length" 443 | ); 444 | if ( 445 | (requestDetails.category === "files" || 446 | requestDetails.category === "custom") && 447 | sizeCellHeader && 448 | Number(sizeCellHeader.value) !== 0 449 | ) { 450 | sizeCell.textContent = formatBytes(sizeCellHeader.value); 451 | sizeCell.title = sizeCellHeader.value; 452 | } else sizeCell.textContent = "-"; 453 | row.appendChild(sizeCell); 454 | 455 | const sourceCell = document.createElement("td"); 456 | sourceCell.textContent = 457 | titlePref && 458 | requestDetails.tabData?.title && 459 | // tabData.title falls back to url 460 | !requestDetails.url.includes(requestDetails.tabData.title) 461 | ? requestDetails.tabData.title 462 | : requestDetails.hostname; 463 | sourceCell.title = 464 | requestDetails.documentUrl || 465 | requestDetails.originUrl || 466 | requestDetails.initiator || 467 | requestDetails.tabData.url; 468 | row.appendChild(sourceCell); 469 | 470 | const timestampCell = document.createElement("td"); 471 | timestampCell.textContent = getTimestamp(requestDetails.timeStamp); 472 | row.appendChild(timestampCell); 473 | } 474 | 475 | const deleteCell = document.createElement("td"); 476 | const deleteX = document.createElement("a"); 477 | deleteX.textContent = "✖"; 478 | deleteX.href = ""; 479 | deleteX.style.textDecoration = "none"; 480 | deleteX.onclick = (e) => { 481 | e.preventDefault(); 482 | e.stopPropagation(); 483 | deleteURL(requestDetails); 484 | }; 485 | deleteCell.onclick = (e) => { 486 | e.preventDefault(); 487 | deleteURL(requestDetails); 488 | }; 489 | deleteX.onfocus = () => (urlCell.style.textDecoration = "line-through"); 490 | deleteX.onblur = () => (urlCell.style.textDecoration = "initial"); 491 | deleteCell.onmouseover = () => 492 | (urlCell.style.textDecoration = "line-through"); 493 | deleteCell.onmouseout = () => (urlCell.style.textDecoration = "initial"); 494 | deleteCell.style.cursor = "pointer"; 495 | deleteCell.title = _("deleteTooltip"); 496 | deleteCell.appendChild(deleteX); 497 | row.appendChild(deleteCell); 498 | 499 | table.appendChild(row); 500 | } 501 | }; 502 | 503 | const insertPlaceholder = () => { 504 | document.getElementById("copyAll").disabled = true; 505 | document.getElementById("clearList").disabled = true; 506 | if (!document.getElementById("filterInput").value) 507 | document.getElementById("filterInput").disabled = true; 508 | document.getElementById("headers").style.display = "none"; 509 | 510 | const row = document.createElement("tr"); 511 | 512 | const placeholderCell = document.createElement("td"); 513 | placeholderCell.colSpan = document.getElementsByTagName("th").length; // i would never remember to update this manually 514 | placeholderCell.textContent = _("placeholderCell"); 515 | 516 | row.appendChild(placeholderCell); 517 | 518 | table.appendChild(row); 519 | }; 520 | 521 | const urlStorage = await getStorage("urlStorage"); 522 | const urlStorageRestore = await getStorage("urlStorageRestore"); 523 | 524 | if (urlStorage.length || urlStorageRestore.length) { 525 | const urlStorageFilter = document 526 | .getElementById("filterInput") 527 | .value.toLowerCase(); 528 | 529 | // do the query first to avoid async issues 530 | chrome.tabs.query({ active: true, currentWindow: true }, (tab) => { 531 | if (document.getElementById("tabThis").checked) { 532 | urlList = urlStorage 533 | ? urlStorage.filter((url) => url.tabId === tab[0].id) 534 | : []; 535 | } else if (document.getElementById("tabAll").checked) { 536 | urlList = urlStorage 537 | ? urlStorage.filter( 538 | (url) => url.tabData?.incognito === tab[0].incognito 539 | ) 540 | : []; 541 | } else if (document.getElementById("tabPrevious").checked) { 542 | urlList = urlStorageRestore || []; 543 | } 544 | 545 | urlList = urlList.length && urlList.reverse(); // latest entries first 546 | 547 | if (urlStorageFilter) 548 | urlList = 549 | urlList.length && 550 | urlList.filter( 551 | (url) => 552 | url.filename.toLowerCase().includes(urlStorageFilter) || 553 | url.tabData?.title?.toLowerCase().includes(urlStorageFilter) || 554 | url.type.toLowerCase().includes(urlStorageFilter) || 555 | url.hostname.toLowerCase().includes(urlStorageFilter) 556 | ); 557 | 558 | if (recentPref && urlList.length > recentAmount) 559 | urlList.length = recentAmount; 560 | 561 | // clear list first just in case - quick and dirty 562 | table.innerHTML = ""; 563 | 564 | urlList.length 565 | ? insertList(urlList) // latest entries first 566 | : insertPlaceholder(); 567 | }); 568 | } else { 569 | table.innerHTML = ""; 570 | insertPlaceholder(); 571 | } 572 | }; 573 | 574 | const saveOption = (e) => { 575 | if (e.target.type === "radio") createList(); 576 | saveOptionStorage(e, document.getElementsByClassName("option")); 577 | }; 578 | 579 | const restoreOptions = async () => { 580 | titlePref = await getStorage("titlePref"); 581 | filenamePref = await getStorage("filenamePref"); 582 | timestampPref = await getStorage("timestampPref"); 583 | downloadDirectPref = await getStorage("downloadDirectPref"); 584 | newline = await getStorage("newline"); 585 | recentPref = await getStorage("recentPref"); 586 | recentAmount = await getStorage("recentAmount"); 587 | noRestorePref = await getStorage("noRestorePref"); 588 | 589 | const options = document.getElementsByClassName("option"); 590 | for (const option of options) { 591 | option.onchange = (e) => saveOption(e); 592 | if ((await getStorage(option.id)) !== null) { 593 | if ( 594 | document.getElementById(option.id).type === "checkbox" || 595 | document.getElementById(option.id).type === "radio" 596 | ) 597 | document.getElementById(option.id).checked = await getStorage( 598 | option.id 599 | ); 600 | else 601 | document.getElementById(option.id).value = await getStorage(option.id); 602 | } 603 | } 604 | }; 605 | 606 | document.addEventListener("DOMContentLoaded", async () => { 607 | // reset badge when clicked 608 | if (document.body.id === "popup") { 609 | chrome.browserAction.setBadgeBackgroundColor({ color: "silver" }); 610 | chrome.browserAction.setBadgeText({ text: "" }); 611 | // workaround to detect popup close 612 | chrome.runtime.connect({ name: "popup" }); 613 | } 614 | 615 | await restoreOptions(); 616 | 617 | // i18n 618 | const labels = document.getElementsByTagName("label"); 619 | for (const label of labels) { 620 | label.textContent = _(label.htmlFor); 621 | } 622 | const selectOptions = document.getElementsByTagName("option"); 623 | for (const selectOption of selectOptions) { 624 | if (!selectOption.textContent) 625 | selectOption.textContent = _(selectOption.value); 626 | } 627 | 628 | // button and text input functionality 629 | document.getElementById("copyAll").onclick = (e) => { 630 | e.preventDefault(); 631 | copyAll(); 632 | }; 633 | 634 | document.getElementById("clearList").onclick = (e) => { 635 | e.preventDefault(); 636 | clearList(); 637 | }; 638 | 639 | document.getElementById("openOptions").onclick = (e) => { 640 | e.preventDefault(); 641 | chrome.runtime.openOptionsPage(); 642 | }; 643 | 644 | document.getElementById("filterInput").onkeyup = () => { 645 | createList(); 646 | if (!document.getElementById("filterInput").value) { 647 | document.getElementById("clearFilterInput").disabled = true; 648 | document.getElementById("clearFilterInput").style.cursor = "default"; 649 | } else { 650 | document.getElementById("clearFilterInput").disabled = false; 651 | document.getElementById("clearFilterInput").style.cursor = "pointer"; 652 | } 653 | }; 654 | 655 | if (!document.getElementById("filterInput").value) { 656 | document.getElementById("clearFilterInput").disabled = true; 657 | document.getElementById("clearFilterInput").style.cursor = "default"; 658 | } else { 659 | document.getElementById("clearFilterInput").disabled = false; 660 | document.getElementById("clearFilterInput").style.cursor = "pointer"; 661 | } 662 | 663 | document.getElementById("clearFilterInput").onclick = () => { 664 | document.getElementById("filterInput").value = ""; 665 | setStorage({ filterInput: "" }); 666 | createList(); 667 | document.getElementById("clearFilterInput").style.cursor = "default"; 668 | }; 669 | 670 | if (noRestorePref) { 671 | if (document.getElementById("tabPrevious").checked) 672 | document.getElementById("tabAll").checked = true; 673 | document.getElementById("tabPrevious").parentElement.style.display = "none"; 674 | } 675 | createList(); 676 | 677 | chrome.runtime.onMessage.addListener((message) => { 678 | if (message.urlStorage) createList(); 679 | if (document.body.id === "sidebar" && message.options) restoreOptions(); 680 | }); 681 | }); 682 | -------------------------------------------------------------------------------- /src/manifest-chrome.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "The Stream Detector", 3 | "version": "2.11.7", 4 | "description": "__MSG_extText__", 5 | "manifest_version": 3, 6 | "default_locale": "en", 7 | "permissions": [ 8 | "clipboardWrite", 9 | "downloads", 10 | "notifications", 11 | "storage", 12 | "tabs", 13 | "webRequest" 14 | ], 15 | "host_permissions": [""], 16 | "background": { 17 | "service_worker": "js/background.js", 18 | "type": "module" 19 | }, 20 | "icons": { 21 | "16": "img/icon-dark-16.png", 22 | "48": "img/icon-dark-48.png", 23 | "96": "img/icon-dark-96.png" 24 | }, 25 | "commands": { 26 | "open-popup": { 27 | "description": "__MSG_openPopup__" 28 | }, 29 | "open-sidebar": { 30 | "description": "__MSG_openSidebar__" 31 | } 32 | }, 33 | "options_ui": { 34 | "page": "options.html" 35 | }, 36 | "action": { 37 | "default_title": "The Stream Detector", 38 | "default_popup": "popup.html", 39 | "default_icon": { 40 | "16": "img/icon-dark-16.png", 41 | "48": "img/icon-dark-48.png" 42 | }, 43 | "theme_icons": [ 44 | { 45 | "dark": "img/icon-dark-96.png", 46 | "light": "img/icon-light-96.png", 47 | "size": 96 48 | }, 49 | { 50 | "dark": "img/icon-dark-48.png", 51 | "light": "img/icon-light-48.png", 52 | "size": 48 53 | }, 54 | { 55 | "dark": "img/icon-dark-16.png", 56 | "light": "img/icon-light-16.png", 57 | "size": 16 58 | } 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/manifest-firefox.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "The Stream Detector", 3 | "version": "2.11.7", 4 | "description": "__MSG_extText__", 5 | "manifest_version": 2, 6 | "default_locale": "en", 7 | "permissions": [ 8 | "", 9 | "clipboardWrite", 10 | "downloads", 11 | "notifications", 12 | "storage", 13 | "tabs", 14 | "webRequest" 15 | ], 16 | "background": { 17 | "scripts": ["js/background.js"], 18 | "persistent": false 19 | }, 20 | "icons": { 21 | "16": "img/icon-dark-16.png", 22 | "48": "img/icon-dark-48.png", 23 | "96": "img/icon-dark-96.png" 24 | }, 25 | "commands": { 26 | "open-popup": { 27 | "description": "__MSG_openPopup__" 28 | }, 29 | "open-sidebar": { 30 | "description": "__MSG_openSidebar__" 31 | } 32 | }, 33 | "options_ui": { 34 | "page": "options.html" 35 | }, 36 | "browser_action": { 37 | "default_title": "The Stream Detector", 38 | "default_popup": "popup.html", 39 | "default_icon": { 40 | "16": "img/icon-dark-16.png", 41 | "48": "img/icon-dark-48.png" 42 | }, 43 | "theme_icons": [ 44 | { 45 | "dark": "img/icon-dark-96.png", 46 | "light": "img/icon-light-96.png", 47 | "size": 96 48 | }, 49 | { 50 | "dark": "img/icon-dark-48.png", 51 | "light": "img/icon-light-48.png", 52 | "size": 48 53 | }, 54 | { 55 | "dark": "img/icon-dark-16.png", 56 | "light": "img/icon-light-16.png", 57 | "size": 16 58 | } 59 | ] 60 | }, 61 | "sidebar_action": { 62 | "default_title": "The Stream Detector", 63 | "default_panel": "sidebar.html", 64 | "default_icon": { 65 | "16": "img/icon-dark-16.png", 66 | "48": "img/icon-dark-48.png" 67 | }, 68 | "open_at_install": false 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | The Stream Detector 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | 35 | 36 | 37 | 38 | 44 | 45 | 46 | 47 | 59 | 60 | 61 | 62 | 68 | 69 | 70 | 71 | 86 | 87 | 88 | 89 | 100 | 101 | 102 | 103 | 114 | 115 | 116 | 125 | 126 | 127 | 136 | 137 | 138 | 139 | 151 | 152 | 153 | 165 | 166 | 167 | 168 | 179 | 180 | 181 | 182 | 193 | 194 | 195 | 196 | 199 | 200 | 201 | 202 | 205 | 206 | 207 | 208 | 211 | 212 | 213 | 214 | 217 | 218 | 219 | 220 | 227 | 228 | 229 | 230 | 236 | 237 | 238 | 239 | 250 | 251 | 252 | 253 | 265 | 266 | 267 | 268 | 279 | 280 | 281 | 282 | 293 | 294 | 295 | 296 | 299 | 300 | 301 | 302 | 314 | 315 | 316 | 317 | 320 | 321 | 322 | 323 | 326 | 327 | 328 | 329 | 333 | 334 | 335 | 336 | 339 | 340 | 341 | 342 | 343 | 344 |
15 | 16 |
21 | 22 |
27 | 28 |
33 | 34 |
39 | 40 |
41 | ? 42 |
43 |
48 | 49 |
50 |  MB 57 |
58 |
63 | 64 |
65 | ? 66 |
67 |
72 | 85 |
90 | 91 |
92 | 98 |
99 |
104 | 110 |
111 | ? 112 |
113 |
117 | 118 | 124 |
128 | 129 | 135 |
140 | 141 | /  / 147 |
148 | ! 149 |
150 |
154 | 155 | 161 |
162 | ? 163 |
164 |
169 | 170 | 175 |
176 | ? 177 |
178 |
183 | 184 | 189 |
190 | ? 191 |
192 |
197 | 198 |
203 | 204 |
209 | 210 |
215 | 216 |
221 | 226 |
231 | 235 |
240 | 241 |
242 | 248 |
249 |
254 | 255 |
256 | 263 |
264 |
269 | 270 |
271 | 277 |
278 |
283 | 284 | 289 |
290 | ? 291 |
292 |
297 | 298 |
303 | 304 |
305 | 312 |
313 |
318 | 319 |
324 | 325 |
330 |
337 |
345 | 346 | 347 | 348 | 349 | -------------------------------------------------------------------------------- /src/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | The Stream Detector 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 18 | 22 | 26 | 27 | 28 |
15 | 16 | 19 | 20 | 23 | 24 |
29 | 30 | 31 | 32 | 37 | 53 | 54 | 55 |
33 | 34 | 35 | 36 | 38 | 39 | 52 |
56 |
57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 |
78 |
79 | 80 | 81 | 82 | 86 | 91 | 96 | 101 | 102 | 103 |
83 | 84 | 87 | 90 | 92 | 95 | 97 | 100 |
104 |
105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /src/sidebar.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | The Stream Detector 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 18 | 22 | 26 | 27 | 28 |
15 | 16 | 19 | 20 | 23 | 24 |
29 | 30 | 31 | 32 | 37 | 40 | 55 | 56 | 57 |
33 | 34 | 35 | 36 | 38 | 39 | 41 | 54 |
58 |
59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 |
72 |
73 | 74 | 75 | 76 | 80 | 85 | 90 | 95 | 96 | 97 |
77 | 78 | 81 | 84 | 86 | 89 | 91 | 94 |
98 |
99 | 100 | 101 | 102 | 103 | --------------------------------------------------------------------------------