├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md └── workflows │ ├── addon-validations.yml │ └── make-release.yml ├── .gitignore ├── .patches └── matrix.patch ├── LICENSES ├── GPL-3.0-only └── MIT ├── README.md ├── addon.xml ├── changelog.txt ├── fanart.jpg ├── icon.png └── resources ├── language ├── resource.language.en_gb │ └── strings.po └── resource.language.he_il │ └── strings.po ├── lib ├── __init__.py ├── __run__.py └── addon_lib │ ├── __init__.py │ ├── cache.py │ ├── constants.py │ ├── db_utils.py │ ├── jsunpack.py │ ├── kodi.py │ ├── log_utils.py │ ├── net.py │ ├── playback.py │ ├── remote.py │ ├── routes.py │ ├── strings.py │ ├── url_dispatcher.py │ ├── urlresolver_helpers.py │ └── utils.py ├── media └── screenshots │ ├── screenshot000.jpg │ ├── screenshot001.jpg │ ├── screenshot002.jpg │ ├── screenshot003.jpg │ └── screenshot004.jpg └── settings.xml /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "🐞 Bug Report" 3 | about: Create a bug report to help us improve 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Context 11 | 12 | Please provide any relevant information about your setup 13 | 14 | * Add-on Version: 15 | * Kodi Version: 16 | * Kodi GUI Language: 17 | * Operating System: 18 | * Operating System Language: 19 | 20 | 21 | 22 | ------ 23 | 24 | ### Expected Behavior 25 | 26 | Please describe the behavior you are expecting. 27 | 28 | 29 | 30 | ------ 31 | 32 | ### Current Behavior 33 | 34 | What is the current behavior? 35 | 36 | 37 | 38 | ------ 39 | 40 | ### Steps to Reproduce 41 | 42 | Please provide detailed steps for reproducing the issue. 43 | 44 | 1. 45 | 2. 46 | 3. 47 | 48 | 49 | 50 | ------ 51 | 52 | ### Log 53 | 54 | Please include a complete [debug log](https://kodi.wiki/view/Log_file). 55 | 56 | 57 | 58 | ------ 59 | 60 | ### Additional Information 61 | 62 | Please provide any additional information that may be helpful. 63 | 64 | 65 | 66 | ------ 67 | 68 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "🚀 Feature Request" 3 | about: Request a new feature to help us improve 4 | title: '' 5 | labels: 'enhancement' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### What Problem Does This Solve? 11 | 12 | Please provide a concise description of the problem this request solves. 13 | 14 | 15 | 16 | ------ 17 | 18 | 19 | 20 | ### Suggest a Solution (optional) 21 | 22 | Please provide a concise description of your suggested solution. If there are multiple solutions, describe them independently and optionally follow them with a comparison. 23 | 24 | 25 | 26 | ------ 27 | 28 | -------------------------------------------------------------------------------- /.github/workflows/addon-validations.yml: -------------------------------------------------------------------------------- 1 | name: Add-on Validations 2 | 3 | on: 4 | push: 5 | branches: [ master, main ] 6 | 7 | pull_request: 8 | branches: [ master, main ] 9 | 10 | jobs: 11 | addon-validations: 12 | if: github.repository == 'anxdpanic/plugin.video.playthis' 13 | 14 | runs-on: ubuntu-latest 15 | name: Add-on Validations 16 | 17 | steps: 18 | - name: Checkout Add-on 19 | uses: actions/checkout@v2 20 | with: 21 | path: ${{ github.event.repository.name }} 22 | 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | python -m pip install git+https://github.com/xbmc/addon-check.git 27 | 28 | - name: Kodi Add-on Checker (Jarvis) 29 | id: kodi-addon-checker-jarvis 30 | run: | 31 | kodi-addon-checker ${{ github.event.repository.name }} --branch=jarvis 32 | 33 | - name: Staging for Matrix 34 | run: | 35 | git reset 36 | git checkout . 37 | git clean -fdx 38 | git apply .patches/matrix.patch 39 | working-directory: ${{ github.event.repository.name }} 40 | 41 | - name: Kodi Add-on Checker (Matrix) 42 | id: kodi-addon-checker-matrix 43 | run: | 44 | kodi-addon-checker ${{ github.event.repository.name }} --branch=matrix 45 | -------------------------------------------------------------------------------- /.github/workflows/make-release.yml: -------------------------------------------------------------------------------- 1 | # Based on https://github.com/im85288/service.upnext/blob/master/.github/workflows/release.yml 2 | name: Make Release 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | - '*-dev' 8 | 9 | jobs: 10 | release: 11 | if: github.repository == 'anxdpanic/plugin.video.playthis' 12 | 13 | name: Make Release 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Release Status 18 | id: release 19 | run: | 20 | version=${GITHUB_REF/refs\/tags\//} 21 | if [[ $version == *-dev ]] ; 22 | then 23 | echo ::set-output name=pre-release::true 24 | else 25 | echo ::set-output name=pre-release::false 26 | fi 27 | 28 | - name: Checkout Add-on 29 | uses: actions/checkout@v2 30 | with: 31 | path: ${{ github.event.repository.name }} 32 | 33 | - name: Install dependencies 34 | run: | 35 | sudo apt-get install libxml2-utils xmlstarlet zip 36 | 37 | - name: Get Changelog 38 | id: changelog 39 | run: | 40 | changes=$(xmlstarlet sel -t -v '//news' -n addon.xml) 41 | changes="${changes//'%'/'%25'}" 42 | changes="${changes//$'\n'/'%0A'}" 43 | changes="${changes//$'\r'/'%0D'}" 44 | echo ::set-output name=changes::$changes 45 | working-directory: ${{ github.event.repository.name }} 46 | 47 | - name: Create Zip (Jarvis) 48 | id: zip-jarvis 49 | run: | 50 | git reset 51 | git checkout . 52 | git clean -fdx 53 | mv .git .. 54 | rm -rf .??* 55 | rm *.md 56 | version=$(xmlstarlet sel -t -v 'string(/addon/@version)' addon.xml) 57 | filename=${{ github.event.repository.name }}-${version}.zip 58 | cd .. 59 | zip -r $filename ${{ github.event.repository.name }} 60 | mv .git ${{ github.event.repository.name }} 61 | echo ::set-output name=filename::$filename 62 | working-directory: ${{ github.event.repository.name }} 63 | 64 | - name: Create Zip (Matrix) 65 | id: zip-matrix 66 | run: | 67 | git reset 68 | git checkout . 69 | git clean -fdx 70 | git apply .patches/matrix.patch 71 | mv .git .. 72 | rm -rf .??* 73 | rm *.md 74 | version=$(xmlstarlet sel -t -v 'string(/addon/@version)' addon.xml) 75 | xmlstarlet ed -L -u '/addon/@version' -v "${version}+matrix.1" addon.xml 76 | version=$(xmlstarlet sel -t -v 'string(/addon/@version)' addon.xml) 77 | filename=${{ github.event.repository.name }}-${version}.zip 78 | cd .. 79 | zip -r $filename ${{ github.event.repository.name }} 80 | mv .git ${{ github.event.repository.name }} 81 | echo ::set-output name=filename::$filename 82 | working-directory: ${{ github.event.repository.name }} 83 | 84 | - name: Create Release 85 | id: create-release 86 | uses: actions/create-release@v1 87 | env: 88 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 89 | with: 90 | tag_name: ${{ github.ref }} 91 | release_name: ${{ github.ref }} 92 | body: ${{ steps.changelog.outputs.changes }} 93 | draft: false 94 | prerelease: ${{ steps.release.outputs.pre-release }} 95 | 96 | - name: Upload Zip (Jarvis) 97 | id: upload-jarvis 98 | uses: actions/upload-release-asset@v1 99 | env: 100 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 101 | with: 102 | upload_url: ${{ steps.create-release.outputs.upload_url }} 103 | asset_name: ${{ steps.zip-jarvis.outputs.filename }} 104 | asset_path: ${{ steps.zip-jarvis.outputs.filename }} 105 | asset_content_type: application/zip 106 | 107 | - name: Upload Zip (Matrix) 108 | id: upload-matrix 109 | uses: actions/upload-release-asset@v1 110 | env: 111 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 112 | with: 113 | upload_url: ${{ steps.create-release.outputs.upload_url }} 114 | asset_name: ${{ steps.zip-matrix.outputs.filename }} 115 | asset_path: ${{ steps.zip-matrix.outputs.filename }} 116 | asset_content_type: application/zip 117 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.settings 2 | /.project 3 | /.pydevproject 4 | *.pyc 5 | *.pyo 6 | /.idea 7 | -------------------------------------------------------------------------------- /.patches/matrix.patch: -------------------------------------------------------------------------------- 1 | From dd9d6f9efa317a19c3c78735999fc984c0fc6580 Mon Sep 17 00:00:00 2001 2 | From: anxdpanic 3 | Date: Sun, 29 Mar 2020 13:52:17 -0400 4 | Subject: [PATCH] =?UTF-8?q?=EF=BB=BFrequired=20matrix=20changes?= 5 | MIME-Version: 1.0 6 | Content-Type: text/plain; charset=UTF-8 7 | Content-Transfer-Encoding: 8bit 8 | 9 | --- 10 | addon.xml | 2 +- 11 | 1 file changed, 1 insertion(+), 1 deletion(-) 12 | 13 | diff --git a/addon.xml b/addon.xml 14 | index 35d7d1f..001ff8d 100644 15 | --- a/addon.xml 16 | +++ b/addon.xml 17 | @@ -3,5 +3,5 @@ 18 | 19 | - 20 | + 21 | 22 | 23 | 24 | -- 25 | 2.26.2.windows.1 26 | 27 | From e184a589cbdb0f8ffb2ebec50266761b7fd964b9 Mon Sep 17 00:00:00 2001 28 | From: anxdpanic 29 | Date: Fri, 11 Sep 2020 13:49:44 -0400 30 | Subject: [PATCH] =?UTF-8?q?=EF=BB=BFfix=20settings=20related=20crash?= 31 | MIME-Version: 1.0 32 | Content-Type: text/plain; charset=UTF-8 33 | Content-Transfer-Encoding: 8bit 34 | 35 | --- 36 | resources/settings.xml | 236 ++++++++++++++++++++++++++++++++++------- 37 | 1 file changed, 200 insertions(+), 36 deletions(-) 38 | 39 | diff --git a/resources/settings.xml b/resources/settings.xml 40 | index 762a77f..8eb1569 100644 41 | --- a/resources/settings.xml 42 | +++ b/resources/settings.xml 43 | @@ -1,37 +1,201 @@ 44 | - 45 | - 46 | - 47 | - 48 | - 49 | - 50 | - 51 | - 52 | - 53 | - 54 | - 55 | - 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 | +
83 | + 84 | + 85 | + 86 | + 0 87 | + 1 88 | + 89 | + 90 | + 91 | + 92 | + 93 | + 94 | + 95 | + 96 | + 97 | + 0 98 | + 150 99 | + 100 | + 0 101 | + 500 102 | + 103 | + 104 | + false 105 | + 106 | + 107 | + 108 | + 0 109 | + true 110 | + 111 | + 112 | + 113 | + 114 | + 115 | + 0 116 | + RunPlugin(plugin://plugin.video.playthis/?mode=clearhistory) 117 | + 118 | + true 119 | + 120 | + 121 | + 122 | + 123 | + 124 | + 125 | + 126 | + 127 | + 0 128 | + 1 129 | + 130 | + 0 131 | + 8 132 | + 133 | + 134 | + false 135 | + 136 | + 137 | + 138 | + 0 139 | + RunPlugin(plugin://plugin.video.playthis/?mode=clearcache) 140 | + 141 | + true 142 | + 143 | + 144 | + 145 | + 146 | + 0 147 | + RunPlugin(plugin://plugin.video.playthis/?mode=clearcookies) 148 | + 149 | + true 150 | + 151 | + 152 | + 153 | + 154 | + 155 | + 156 | + 157 | + 158 | + 0 159 | + 160 | + 161 | + 162 | + 163 | + true 164 | + 165 | + 166 | + 30706 167 | + 168 | + 169 | + 170 | + 0 171 | + 172 | + 173 | + 174 | + 175 | + true 176 | + 177 | + 178 | + 30715 179 | + 180 | + 181 | + 182 | + 183 | + 184 | + 185 | + 186 | + 0 187 | + RunPlugin(plugin://plugin.video.playthis/?mode=ytdl) 188 | + 189 | + true 190 | + 191 | + 192 | + true 193 | + 194 | + 195 | + 196 | + 0 197 | + 198 | + 199 | + true 200 | + 201 | + 202 | + 203 | + true 204 | + 205 | + 206 | + false 207 | + 208 | + 209 | + 210 | + 211 | + 212 | + 213 | + 214 | + 0 215 | + 216 | + 217 | + true 218 | + 219 | + 220 | + 221 | + true 222 | + 223 | + 224 | + false 225 | + 226 | + 227 | + 228 | + 229 | + 230 | + 231 | + 232 | + 233 | + 234 | + 235 | + 236 | + 0 237 | + 238 | + 239 | + true 240 | + 241 | + 242 | + 30717 243 | + 244 | + 245 | + 246 | + 0 247 | + 8080 248 | + 249 | + 30718 250 | + 251 | + 252 | + 253 | + 0 254 | + kodi 255 | + 256 | + 30719 257 | + 258 | + 259 | + 260 | + 0 261 | + 262 | + 263 | + true 264 | + 265 | + 266 | + 30720 267 | + true 268 | + 269 | + 270 | + 271 | + 0 272 | + true 273 | + 274 | + 275 | + 276 | + 277 | + 278 | + 279 | +
280 |
281 | -- 282 | 2.26.2.windows.1 283 | -------------------------------------------------------------------------------- /LICENSES/GPL-3.0-only: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /LICENSES/MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), 2 | to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 3 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 8 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 9 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![PlayThis](icon.png) 2 | # PlayThis 3 | 4 | [![Build Status](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Fanxdpanic%2Fplugin.video.playthis%2Fbadge&logo=none)](https://actions-badge.atrox.dev/anxdpanic/plugin.video.playthis/goto) 5 | ![License](https://img.shields.io/badge/license-GPL--3.0--only-success.svg) 6 | ![Kodi Version](https://img.shields.io/badge/kodi-jarvis%2B-success.svg) 7 | ![Contributors](https://img.shields.io/github/contributors/anxdpanic/plugin.video.playthis.svg) 8 | 9 | The PlayThis add-on will attempt to find and resolve1 media from a url to play or open. A history list is available for future use, exporting to .m3u/.strm2 and sending to a remote PlayThis add-on. Supports video, audio, images and executable3. 10 | * 1 resolves using ResolveURL(optional) or URLResolver(optional), and youtube-dl 11 | * 2 M3U only usable in Kodi w/ PlayThis installed 12 | * 3 'executable' items are urls with potential results available through scraping 13 | 14 | 15 | - Installation 16 | - 17 | * Kodi 17+: Enable - `Settings -> System -> Add-ons -> Unknown Sources` 18 | 1. Download repository 19 | - Kodi 16-18: [repository.anxdpanic-x.x.x.zip](https://panicked.xyz/repositories/repository.anxdpanic-2.0.0.zip) 20 | - Kodi 19: [repository.anxdpanic-x.x.x+matrix.1.zip](https://panicked.xyz/repositories/matrix/repository.anxdpanic-2.0.0+matrix.1.zip) 21 | 2. [Install from zip file](http://kodi.wiki/view/Add-on_manager#How_to_install_from_a_ZIP_file) (repository.anxdpanic-x.x.x.zip) 22 | 3. [Install from repository](http://kodi.wiki/view/add-on_manager#How_to_install_add-ons_from_a_repository) (anxdpanic Add-on Repository) 23 | 24 | - Usage 25 | - 26 | 27 | Enter url in dialog, choose from history, send url from web browser, add to favorites from history/M3U, curate history list and export to M3U, or create/export a strm. 28 | 29 | _**example.strm**_ 30 | ``` 31 | plugin://plugin.video.playthis/?mode=play&player=false&path=http%3A%2F%2Fwww.dailymotion.com%2Fvideo%2Fx3ol7gj_incredible-freefall-skydiving-over-rio-de-janeiro_sport 32 | ``` 33 | 34 | _**Google Chrome Context Menu**_ 35 | 36 | - Download extension from [Chrome Web Store](https://chrome.google.com/webstore/detail/playthis/adddkaonokkecefokdanjpaamfajogel) 37 | - GitHub: [PlayThis \(Google Chrome\)](https://github.com/anxdpanic/PlayThis-Extension/tree/chrome#playthis-google-chrome) 38 | 39 | _**Firefox 53+ Context Menu**_ 40 | 41 | - Download extension from [AMO Gallery](https://addons.mozilla.org/en-US/firefox/addon/playthis/) 42 | - GitHub: [PlayThis \(Firefox\)](https://github.com/anxdpanic/PlayThis-Extension/tree/firefox#playthis-firefox) 43 | 44 | - Support 45 | - 46 | 47 | Post an [Issue](https://github.com/anxdpanic/plugin.video.playthis/issues) 48 | --- 49 | 50 | Special thanks to [@konsumer420](https://twitter.com/konsumer420) for the icons/artwork 51 | -------------------------------------------------------------------------------- /addon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | executable video audio image 11 | 12 | 13 | 14 | [fix] Kodi 19 not working 15 | [fix] remove LOGNOTICE AND LOGSEVERE, both removed in Kodi 19 16 | [fix] use xbmcvfs.translatePath when available 17 | [fix] mitigate busy dialog related crashes 18 | 19 | 20 | 21 | icon.png 22 | fanart.jpg 23 | resources/media/screenshots/screenshot000.jpg 24 | resources/media/screenshots/screenshot001.jpg 25 | resources/media/screenshots/screenshot002.jpg 26 | resources/media/screenshots/screenshot003.jpg 27 | resources/media/screenshots/screenshot004.jpg 28 | 29 | Find and resolve media from a url to play or open. 30 | 31 | PlayThis will attempt to find and resolve* media from a url to play or open. A history list is available for future use, exporting to .m3u/.strm** and sending to a remote PlayThis add-on. Supports video, audio, images and executable***. 32 | * resolves using youtube-dl 33 | ** exported .m3u/.strm is only usable in Kodi w/ PlayThis installed 34 | *** 'executable' items are urls with potential results available through scraping 35 | 36 | Companion browser extension available on the Chrome Web Store and Mozilla's AMO Gallery 37 | 38 | Artwork by @konsumer420 39 | 40 | all 41 | GPL-3.0-only 42 | https://github.com/anxdpanic/plugin.video.playthis#playthis 43 | https://github.com/anxdpanic/plugin.video.playthis 44 | true 45 | 46 | 47 | -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | 5.0.3 2 | [fix] Kodi 19 not working 3 | [fix] remove LOGNOTICE AND LOGSEVERE, both removed in Kodi 19 4 | [fix] use xbmcvfs.translatePath when available 5 | [fix] mitigate busy dialog related crashes 6 | 7 | 5.0.2 8 | [upd] Use new settings format on Kodi 19 9 | 10 | 5.0.1 11 | [fix] progress dialog in Kodi 19 nightlies 12 | [fix] inputstream property in Kodi 19 nightlies 13 | [fix] handling of inputstream.adaptive not being installed 14 | 15 | 5.0.0 16 | [chg] only use youtube-dl to resolve content 17 | 18 | 4.0.1 19 | [rem] URLResolver as optional dependency 20 | 21 | 4.0.0 22 | [add] use YouTube add-on to resolve YouTube links if the add-on is available 23 | [add] reuse language invoker 24 | [add] Python 3 compat. 25 | [upd] use SPDX license identifiers 26 | [upd] resolve inspections and cosmetics 27 | 28 | 3.5.0 29 | - URLResolver is now optional 30 | 31 | 3.4.6 32 | - cosmetic 33 | 34 | 3.4.5 35 | - fix resolving urls with spaces 36 | 37 | 3.4.4 38 | - fix Facebook videos having no audio 39 | 40 | 3.4.3 41 | - update reddit video resolver 42 | - fix m3u8 check missing urls with parameters 43 | - add shout: to direct play 44 | - filter .css 45 | - remove unused dialog 46 | 47 | 3.4.2 48 | - fix executable items 49 | 50 | 3.4.1 51 | - update strings path 52 | - add debug log to failed head requests 53 | - add reddit resolver 54 | 55 | 3.4.0 56 | - Kodi 16 + required 57 | - use inputstream.adaptive for hls if supported 58 | - expand check for dash content type 59 | - fix single source return 60 | 61 | 3.3.0.6 62 | - minor refactor of xxx 63 | 64 | 3.3.0.5 65 | - add xxx smu resolvers 66 | 67 | 3.3.0.3 68 | - remove external resolver plugins 69 | - resolve requiring to back out of add-on twice 70 | 71 | 3.3.0.1 72 | - add external resolvers 73 | 74 | 3.2.2 75 | - resolve breaking change from urlresolver 76 | 77 | 3.2.1 78 | - jsonrpc now uses POST instead of GET 79 | 80 | 3.2.0 81 | - add inputstream.rtmp and inputstream.hls 82 | - remove twitch from runplugin exceptions 83 | 84 | 3.1.1 85 | - add Setting to 'Remote/Send' - 'Resolve/scrape locally' 86 | - minor progress dialog fixes 87 | - minor string updates 88 | 89 | 3.1.0 90 | - add 'Send to remote PlayThis' 91 | - add 'Export to .strm' 92 | - add thumbnail support 93 | - performance improvement 94 | - update and improve dash support 95 | - update context menu layout 96 | - change cookie handling 97 | - change settings category 'Cache' -> 'Maintenance', and add 'Clear Cookies' 98 | - other minor adjusts, bug fixes, refactoring 99 | 100 | 3.0.0 101 | - add generalized scraping for sources as fallback if url does not resolve 102 | - add dash and smil support 103 | - add basic mimetype detection 104 | - add content type support (video audio image executable) 105 | - add attempt to identify and support quicktime atom ** thanks @tknorris 106 | - increased history size limits, applies to ALL content types as use cases my still be limited to one type 107 | - export to M3U exports content specific list 108 | - delay adding to history until potentially valid source found 109 | - add labels/rename 110 | - add function caching, expire time and reset in settings 111 | - add youtube-dl support 112 | - add progress dialog 113 | 114 | 2.2.5 115 | - export to .m3u now exports PlayThis plugin url instead of resolved url 116 | allows resolution at playback, resolves expired session/token/ip issues, m3u only usable in Kodi w/ PlayThis 117 | 118 | 2.2.3 119 | - external resolver updates 120 | + ol, use api until updated in urlresolver 121 | - twitch, included in urlresolver 122 | 123 | 2.2.2 124 | - add 'Add to history on play' to Settings 125 | - reorganize Settings menu 126 | 127 | 2.2.1 128 | - fix error on 'New ...' if no input 129 | 130 | 2.2.0 131 | - delete now uses row_id instead of url 132 | - add Export to M3U 133 | 134 | 2.1.3 135 | - add route to add to history without playback 136 | - ?mode=add&path=http%3A%2F%2Fexample.com%2F 137 | 138 | 2.1.2 139 | - twitch resolver updates 140 | 141 | 2.1.1 142 | - add URLResolver Settings action to Settings 143 | - add twitch resolver, resolve twitch urls and play using plugin.video.twitch 144 | 145 | 2.1.0 146 | - add directory listing of history 147 | - add 'Delete from history' context menu for directory listing 148 | - enables 'Delete from history', 'Add to favorites', and the rest of the Kodi default context items 149 | - set directory listing to default 150 | - add optional &history=true/false to mode=play 151 | - history=true default, keeps history of items played from plugin url 152 | 153 | 2.0.2 154 | - fix playback from strm files 155 | - changes to url/route for strm files |req'd| &player=false 156 | 157 | 2.0.1 158 | - fix improper resolver issue 159 | 160 | 2.0.0 161 | - re-write of smokdpi's PlayThis 162 | -------------------------------------------------------------------------------- /fanart.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anxdpanic/plugin.video.playthis/8ce16be445f784a6dd6c60a8bc2ea0b95765d728/fanart.jpg -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anxdpanic/plugin.video.playthis/8ce16be445f784a6dd6c60a8bc2ea0b95765d728/icon.png -------------------------------------------------------------------------------- /resources/language/resource.language.en_gb/strings.po: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Addon Name: PlayThis 3 | # Addon id: plugin.video.playthis 4 | # Addon Provider: anxdpanic 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version:plugin.video.playthis\n" 8 | "POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" 9 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 10 | "Language-Team: English\n" 11 | "MIME-Version: 1.0\n" 12 | "Content-Type: text/plain; charset=UTF-8\n" 13 | "Content-Transfer-Encoding: 8bit\n" 14 | "Language: en_gb\n" 15 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 16 | 17 | msgctxt "#30100" 18 | msgid "Enter URL/Path for playback" 19 | msgstr "" 20 | 21 | msgctxt "#30101" 22 | msgid "New ..." 23 | msgstr "" 24 | 25 | msgctxt "#30102" 26 | msgid "Clear history" 27 | msgstr "" 28 | 29 | msgctxt "#30103" 30 | msgid "History has been cleared" 31 | msgstr "" 32 | 33 | msgctxt "#30104" 34 | msgid "Choose item for playback" 35 | msgstr "" 36 | 37 | msgctxt "#30105" 38 | msgid "Failed to clear history" 39 | msgstr "" 40 | 41 | msgctxt "#30106" 42 | msgid "Failed to delete url" 43 | msgstr "" 44 | 45 | msgctxt "#30107" 46 | msgid "Delete" 47 | msgstr "" 48 | 49 | msgctxt "#30108" 50 | msgid "No item(s) to export" 51 | msgstr "" 52 | 53 | msgctxt "#30109" 54 | msgid "Item(s) exported successfully" 55 | msgstr "" 56 | 57 | msgctxt "#30110" 58 | msgid ".m3u Filename" 59 | msgstr "" 60 | 61 | msgctxt "#30111" 62 | msgid "Export list to .m3u" 63 | msgstr "" 64 | 65 | msgctxt "#30112" 66 | msgid "Failed to export item(s)" 67 | msgstr "" 68 | 69 | msgctxt "#30113" 70 | msgid "Choose source" 71 | msgstr "" 72 | 73 | msgctxt "#30114" 74 | msgid "Unknown" 75 | msgstr "" 76 | 77 | msgctxt "#30115" 78 | msgid "Refresh" 79 | msgstr "" 80 | 81 | msgctxt "#30116" 82 | msgid "Clear %s history?" 83 | msgstr "" 84 | 85 | msgctxt "#30117" 86 | msgid "Confirm" 87 | msgstr "" 88 | 89 | msgctxt "#30118" 90 | msgid "Rename" 91 | msgstr "" 92 | 93 | msgctxt "#30119" 94 | msgid "Input new label" 95 | msgstr "" 96 | 97 | msgctxt "#30120" 98 | msgid "Rename failed" 99 | msgstr "" 100 | 101 | msgctxt "#30121" 102 | msgid "Function cache has been reset" 103 | msgstr "" 104 | 105 | msgctxt "#30122" 106 | msgid "Function cache reset failed" 107 | msgstr "" 108 | 109 | msgctxt "#30123" 110 | msgid "Reset function cache?" 111 | msgstr "" 112 | 113 | msgctxt "#30124" 114 | msgid "Resolution completed" 115 | msgstr "" 116 | 117 | msgctxt "#30125" 118 | msgid "Source" 119 | msgstr "" 120 | 121 | msgctxt "#30126" 122 | msgid "Resolution successful" 123 | msgstr "" 124 | 125 | msgctxt "#30127" 126 | msgid "Attempting to resolve with" 127 | msgstr "" 128 | 129 | msgctxt "#30128" 130 | msgid "Attempting to determine type" 131 | msgstr "" 132 | 133 | msgctxt "#30129" 134 | msgid "Resolving" 135 | msgstr "" 136 | 137 | msgctxt "#30130" 138 | msgid "Scraping for potentially supported media urls" 139 | msgstr "" 140 | 141 | msgctxt "#30131" 142 | msgid "Added" 143 | msgstr "" 144 | 145 | msgctxt "#30132" 146 | msgid "Discarded" 147 | msgstr "" 148 | 149 | msgctxt "#30133" 150 | msgid "Preparing results" 151 | msgstr "" 152 | 153 | msgctxt "#30134" 154 | msgid "Support potential" 155 | msgstr "" 156 | 157 | msgctxt "#30135" 158 | msgid "Replaced with" 159 | msgstr "" 160 | 161 | msgctxt "#30136" 162 | msgid "Using media type" 163 | msgstr "" 164 | 165 | msgctxt "#30137" 166 | msgid "Checking for potential support" 167 | msgstr "" 168 | 169 | msgctxt "#30138" 170 | msgid "Clear cookies?" 171 | msgstr "" 172 | 173 | msgctxt "#30139" 174 | msgid "Clearing cookies successful" 175 | msgstr "" 176 | 177 | msgctxt "#30140" 178 | msgid "Clearing cookies failed" 179 | msgstr "" 180 | 181 | msgctxt "#30141" 182 | msgid ".strm Filename" 183 | msgstr "" 184 | 185 | msgctxt "#30142" 186 | msgid "Export to .strm" 187 | msgstr "" 188 | 189 | msgctxt "#30143" 190 | msgid "Thumbnail source" 191 | msgstr "" 192 | 193 | msgctxt "#30144" 194 | msgid "Change thumbnail by url" 195 | msgstr "" 196 | 197 | msgctxt "#30145" 198 | msgid "Change thumbnail by file" 199 | msgstr "" 200 | 201 | msgctxt "#30146" 202 | msgid "Choose your thumbnail" 203 | msgstr "" 204 | 205 | msgctxt "#30147" 206 | msgid "Input thumbnail location" 207 | msgstr "" 208 | 209 | msgctxt "#30148" 210 | msgid "Failed to change thumbnail" 211 | msgstr "" 212 | 213 | msgctxt "#30149" 214 | msgid "Change thumbnail" 215 | msgstr "" 216 | 217 | msgctxt "#30150" 218 | msgid "Send to remote PlayThis" 219 | msgstr "" 220 | 221 | msgctxt "#30151" 222 | msgid "Remote/Send request successful" 223 | msgstr "" 224 | 225 | msgctxt "#30152" 226 | msgid "Manage" 227 | msgstr "" 228 | 229 | msgctxt "#30700" 230 | msgid "History" 231 | msgstr "" 232 | 233 | msgctxt "#30701" 234 | msgid "History size limit (items)" 235 | msgstr "" 236 | 237 | msgctxt "#30702" 238 | msgid "History selection type" 239 | msgstr "" 240 | 241 | msgctxt "#30703" 242 | msgid "Dialog" 243 | msgstr "" 244 | 245 | msgctxt "#30704" 246 | msgid "Directory" 247 | msgstr "" 248 | 249 | # 30705 250 | 251 | msgctxt "#30706" 252 | msgid "M3U export path" 253 | msgstr "" 254 | 255 | msgctxt "#30707" 256 | msgid "Add to history on play" 257 | msgstr "" 258 | 259 | msgctxt "#30708" 260 | msgid "Export" 261 | msgstr "" 262 | 263 | msgctxt "#30709" 264 | msgid "Resolvers" 265 | msgstr "" 266 | 267 | msgctxt "#30710" 268 | msgid "Reset function cache" 269 | msgstr "" 270 | 271 | msgctxt "#30711" 272 | msgid "Function cache expire time (hours)" 273 | msgstr "" 274 | 275 | msgctxt "#30712" 276 | msgid "youtube-dl settings" 277 | msgstr "" 278 | 279 | msgctxt "#30713" 280 | msgid "Maintenance" 281 | msgstr "" 282 | 283 | msgctxt "#30714" 284 | msgid "Clear cookies" 285 | msgstr "" 286 | 287 | msgctxt "#30715" 288 | msgid "STRM export path" 289 | msgstr "" 290 | 291 | msgctxt "#30716" 292 | msgid "Remote/Send" 293 | msgstr "" 294 | 295 | msgctxt "#30717" 296 | msgid "IP address" 297 | msgstr "" 298 | 299 | msgctxt "#30718" 300 | msgid "Port" 301 | msgstr "" 302 | 303 | msgctxt "#30719" 304 | msgid "Username" 305 | msgstr "" 306 | 307 | msgctxt "#30720" 308 | msgid "Password" 309 | msgstr "" 310 | 311 | msgctxt "#30721" 312 | msgid "This requires PlayThis and the 'Allow remote control via HTTP'[CR]setting to be enabled and configured on your remote Kodi installation." 313 | msgstr "" 314 | 315 | msgctxt "#30722" 316 | msgid "[CR]Settings → Services → Webserver/Control → Allow control of Kodi via HTTP" 317 | msgstr "" 318 | 319 | msgctxt "#30723" 320 | msgid "Resolve/scrape locally" 321 | msgstr "" 322 | 323 | msgctxt "#30724" 324 | msgid "No Video Link Found" 325 | msgstr "" 326 | 327 | -------------------------------------------------------------------------------- /resources/language/resource.language.he_il/strings.po: -------------------------------------------------------------------------------- 1 | # Kodi language file 2 | # Addon Name: PlayThis 3 | # Addon id: plugin.video.playthis 4 | # Addon Provider: anxdpanic 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: plugin.video.playthis\n" 8 | "POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" 9 | "PO-Revision-Date: 2017-09-27 16:04+0300\n" 10 | "Last-Translator: A. Dambledore\n" 11 | "Language-Team: Eng2Heb\n" 12 | "Language: he_IL\n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=UTF-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 17 | 18 | msgctxt "#30100" 19 | msgid "Enter URL/Path for playback" 20 | msgstr "הזן כתובת URL/נתיב לניגון" 21 | 22 | msgctxt "#30101" 23 | msgid "New ..." 24 | msgstr "חדש..." 25 | 26 | msgctxt "#30102" 27 | msgid "Clear history" 28 | msgstr "נקה היסטוריה" 29 | 30 | msgctxt "#30103" 31 | msgid "History has been cleared" 32 | msgstr "ההיסטוריה נוקתה" 33 | 34 | msgctxt "#30104" 35 | msgid "Choose item for playback" 36 | msgstr "בחר פריט לניגון" 37 | 38 | msgctxt "#30105" 39 | msgid "Failed to clear history" 40 | msgstr "נכשל בניקוי ההיסטוריה" 41 | 42 | msgctxt "#30106" 43 | msgid "Failed to delete url" 44 | msgstr "נכשל במחיקת כתובת url" 45 | 46 | msgctxt "#30107" 47 | msgid "Delete" 48 | msgstr "מחק" 49 | 50 | msgctxt "#30108" 51 | msgid "No item(s) to export" 52 | msgstr "אין פריטים לייצוא" 53 | 54 | msgctxt "#30109" 55 | msgid "Item(s) exported successfully" 56 | msgstr "הפריטים יוצאו בהצלחה" 57 | 58 | msgctxt "#30110" 59 | msgid ".m3u Filename" 60 | msgstr "שם קובץ .m3u" 61 | 62 | msgctxt "#30111" 63 | msgid "Export list to .m3u" 64 | msgstr "ייצוא רשימה לפורמט .m3u" 65 | 66 | msgctxt "#30112" 67 | msgid "Failed to export item(s)" 68 | msgstr "נכשל בייצוא פריטים" 69 | 70 | msgctxt "#30113" 71 | msgid "Choose source" 72 | msgstr "בחירת מקור" 73 | 74 | msgctxt "#30114" 75 | msgid "Unknown" 76 | msgstr "לא ידוע" 77 | 78 | msgctxt "#30115" 79 | msgid "Refresh" 80 | msgstr "רענן" 81 | 82 | msgctxt "#30116" 83 | msgid "Clear %s history?" 84 | msgstr "נקה %s היסטוריה?" 85 | 86 | msgctxt "#30117" 87 | msgid "Confirm" 88 | msgstr "אישור" 89 | 90 | msgctxt "#30118" 91 | msgid "Rename" 92 | msgstr "שינוי שם" 93 | 94 | msgctxt "#30119" 95 | msgid "Input new label" 96 | msgstr "הזן תווית חדשה" 97 | 98 | msgctxt "#30120" 99 | msgid "Rename failed" 100 | msgstr "שינוי השם נכשל" 101 | 102 | msgctxt "#30121" 103 | msgid "Function cache has been reset" 104 | msgstr "המטמון נוקה" 105 | 106 | msgctxt "#30122" 107 | msgid "Function cache reset failed" 108 | msgstr "נכשל בניקוי המטמון" 109 | 110 | msgctxt "#30123" 111 | msgid "Reset function cache?" 112 | msgstr "האם לנקות את המטמון?" 113 | 114 | msgctxt "#30124" 115 | msgid "Resolution completed" 116 | msgstr "הרזולוציה הושלמה" 117 | 118 | msgctxt "#30125" 119 | msgid "Source" 120 | msgstr "מקור" 121 | 122 | msgctxt "#30126" 123 | msgid "Resolution successful" 124 | msgstr "רזולוציה הסתיימה בהצלחה" 125 | 126 | msgctxt "#30127" 127 | msgid "Attempting to resolve with" 128 | msgstr "מנסה לפתור עם " 129 | 130 | msgctxt "#30128" 131 | msgid "Attempting to determine type" 132 | msgstr "מנסה לקבוע סוג" 133 | 134 | msgctxt "#30129" 135 | msgid "Resolving" 136 | msgstr "מפענח" 137 | 138 | msgctxt "#30130" 139 | msgid "Scraping for potentially supported media urls" 140 | msgstr "מבצע סקראפינג עבור כתובות אתר של מדיה נתמכת" 141 | 142 | msgctxt "#30131" 143 | msgid "Added" 144 | msgstr "נוסף" 145 | 146 | msgctxt "#30132" 147 | msgid "Discarded" 148 | msgstr "נמחק" 149 | 150 | msgctxt "#30133" 151 | msgid "Preparing results" 152 | msgstr "מכין תוצאות" 153 | 154 | msgctxt "#30134" 155 | msgid "Support potential" 156 | msgstr "תמיכה" 157 | 158 | msgctxt "#30135" 159 | msgid "Replaced with" 160 | msgstr "הוחלף ב-" 161 | 162 | msgctxt "#30136" 163 | msgid "Using media type" 164 | msgstr "משתמש בסוג המדיה" 165 | 166 | msgctxt "#30137" 167 | msgid "Checking for potential support" 168 | msgstr "בודק לקבלת תמיכה פוטנציאלית" 169 | 170 | msgctxt "#30138" 171 | msgid "Clear cookies?" 172 | msgstr "נקה קבצי עוגיות?" 173 | 174 | msgctxt "#30139" 175 | msgid "Clearing cookies successful" 176 | msgstr "ניקוי קבצי עוגיות הסתיים בהצלחה" 177 | 178 | msgctxt "#30140" 179 | msgid "Clearing cookies failed" 180 | msgstr "ניקוי קבצי עוגיות נכשל" 181 | 182 | msgctxt "#30141" 183 | msgid ".strm Filename" 184 | msgstr "שם קובץ .strm" 185 | 186 | msgctxt "#30142" 187 | msgid "Export to .strm" 188 | msgstr "ייצוא לקובץ .strm" 189 | 190 | msgctxt "#30143" 191 | msgid "Thumbnail source" 192 | msgstr "מקור לתמונות ממוזערות" 193 | 194 | msgctxt "#30144" 195 | msgid "Change thumbnail by url" 196 | msgstr "שנה תמונה הממוזערת לפי כתובת url" 197 | 198 | msgctxt "#30145" 199 | msgid "Change thumbnail by file" 200 | msgstr "שנה תמונה הממוזערת לפי קובץ" 201 | 202 | msgctxt "#30146" 203 | msgid "Choose your thumbnail" 204 | msgstr "בחר את התמונה הממוזערת" 205 | 206 | msgctxt "#30147" 207 | msgid "Input thumbnail location" 208 | msgstr "הכנס מיקום של תמונות ממוזערות" 209 | 210 | msgctxt "#30148" 211 | msgid "Failed to change thumbnail" 212 | msgstr "לא ניתן לשנות את תמונה ממוזערת" 213 | 214 | msgctxt "#30149" 215 | msgid "Change thumbnail" 216 | msgstr "שנה תמונה ממוזערת" 217 | 218 | msgctxt "#30150" 219 | msgid "Send to remote PlayThis" 220 | msgstr "לשלוח ל PlayThis מרוחק" 221 | 222 | msgctxt "#30151" 223 | msgid "Remote/Send request successful" 224 | msgstr "הבקשה לשליחה אל PlayThis מרוחק הסתיימה בהצלחה" 225 | 226 | msgctxt "#30152" 227 | msgid "Manage" 228 | msgstr "נהל" 229 | 230 | msgctxt "#30700" 231 | msgid "History" 232 | msgstr "היסטוריה" 233 | 234 | msgctxt "#30701" 235 | msgid "History size limit (items)" 236 | msgstr "מגבלת גודל להיסטוריה (פריטים)" 237 | 238 | msgctxt "#30702" 239 | msgid "History selection type" 240 | msgstr "שיטת בחירה בהיסטוריה" 241 | 242 | msgctxt "#30703" 243 | msgid "Dialog" 244 | msgstr "דו-שיח" 245 | 246 | msgctxt "#30704" 247 | msgid "Directory" 248 | msgstr "תיקייה" 249 | 250 | # 30705 251 | 252 | msgctxt "#30706" 253 | msgid "M3U export path" 254 | msgstr "נתיב ייצוא M3U" 255 | 256 | msgctxt "#30707" 257 | msgid "Add to history on play" 258 | msgstr "הוסף להיסטוריה בזמן ניגון" 259 | 260 | msgctxt "#30708" 261 | msgid "Export" 262 | msgstr "יצוא" 263 | 264 | msgctxt "#30709" 265 | msgid "Resolvers" 266 | msgstr "פותרן - Resolvers" 267 | 268 | msgctxt "#30710" 269 | msgid "Reset function cache" 270 | msgstr "ניקוי לפונקציית המטמון" 271 | 272 | msgctxt "#30711" 273 | msgid "Function cache expire time (hours)" 274 | msgstr "זמן תפוגה לפונקציית המטמון (בשעות)" 275 | 276 | msgctxt "#30712" 277 | msgid "youtube-dl settings" 278 | msgstr "הגדרות: youtube-dl" 279 | 280 | msgctxt "#30713" 281 | msgid "Maintenance" 282 | msgstr "תחזוקה" 283 | 284 | msgctxt "#30714" 285 | msgid "Clear cookies" 286 | msgstr "ניקוי קבצי עוגיות" 287 | 288 | msgctxt "#30715" 289 | msgid "STRM export path" 290 | msgstr "נתיב הייצוא STRM" 291 | 292 | msgctxt "#30716" 293 | msgid "Remote/Send" 294 | msgstr "רחוק/שליחה" 295 | 296 | msgctxt "#30717" 297 | msgid "IP address" 298 | msgstr "כתובת IP" 299 | 300 | msgctxt "#30718" 301 | msgid "Port" 302 | msgstr "פורט" 303 | 304 | msgctxt "#30719" 305 | msgid "Username" 306 | msgstr "שם משתמש" 307 | 308 | msgctxt "#30720" 309 | msgid "Password" 310 | msgstr "ססמה" 311 | 312 | msgctxt "#30721" 313 | msgid "This requires PlayThis and the 'Allow remote control via HTTP'[CR]setting to be enabled and configured on your remote Kodi installation." 314 | msgstr "מחייב את PlayThis ואת ההגדרה 'אפשר שליטה מרחוק באמצעות HTTP' [CR] כדי לאפשר ולהגדיר את תצורת קודי המרוחקת." 315 | 316 | msgctxt "#30722" 317 | msgid "[CR]Settings → Services → Webserver/Control → Allow control of Kodi via HTTP" 318 | msgstr "[CR] הגדרות ← שירותים ← שרותי רשת/בקרה ← אפשר שליטה של קודי דרך HTTP" 319 | 320 | msgctxt "#30723" 321 | msgid "Resolve/scrape locally" 322 | msgstr "פתור/בצע סקרייפ מקומי" 323 | 324 | msgctxt "#30724" 325 | msgid "No Video Link Found" 326 | msgstr "" 327 | -------------------------------------------------------------------------------- /resources/lib/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2016-2019 anxdpanic 5 | 6 | This file is part of PlayThis (plugin.video.playthis) 7 | 8 | SPDX-License-Identifier: GPL-3.0-only 9 | See LICENSES/GPL-3.0-only for more information. 10 | """ 11 | 12 | __all__ = ['addon_lib'] 13 | -------------------------------------------------------------------------------- /resources/lib/__run__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2016-2019 anxdpanic 5 | 6 | This file is part of PlayThis (plugin.video.playthis) 7 | 8 | SPDX-License-Identifier: GPL-3.0-only 9 | See LICENSES/GPL-3.0-only for more information. 10 | """ 11 | 12 | import sys 13 | 14 | from addon_lib import routes 15 | 16 | 17 | if __name__ == '__main__': 18 | routes.run(sys.argv) 19 | -------------------------------------------------------------------------------- /resources/lib/addon_lib/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2016-2019 anxdpanic 5 | 6 | This file is part of PlayThis (plugin.video.playthis) 7 | 8 | SPDX-License-Identifier: GPL-3.0-only 9 | See LICENSES/GPL-3.0-only for more information. 10 | """ 11 | 12 | __all__ = ['resolvers', 'cache', 'constants', 'db_utils', 'jsunpack', 13 | 'kodi', 'log_utils', 'net', 'playback', 'remote', 'strings', 14 | 'url_dispatcher', 'urlresolver_helpers', 'utils', 'routes'] 15 | -------------------------------------------------------------------------------- /resources/lib/addon_lib/cache.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2015-2016 tknorris 5 | Copyright (C) 2016-2019 anxdpanic 6 | 7 | This file is part of PlayThis (plugin.video.playthis) 8 | 9 | SPDX-License-Identifier: GPL-3.0-only 10 | See LICENSES/GPL-3.0-only for more information. 11 | """ 12 | 13 | 14 | import functools 15 | import time 16 | import hashlib 17 | import os 18 | import shutil 19 | 20 | from six.moves import cPickle as pickle 21 | 22 | from . import kodi 23 | from . import log_utils 24 | 25 | 26 | cache_path = kodi.translate_path(os.path.join('special://temp/%s/cache' % kodi.get_id())) 27 | try: 28 | if not os.path.exists(cache_path): 29 | os.makedirs(cache_path) 30 | except Exception as e: 31 | log_utils.log('Failed to create cache: %s: %s' % (cache_path, e), log_utils.LOGWARNING) 32 | 33 | cache_enabled = kodi.get_setting('use_cache') == 'true' 34 | 35 | 36 | def reset_cache(): 37 | try: 38 | shutil.rmtree(cache_path) 39 | return True 40 | except Exception as e: 41 | log_utils.log('Failed to Reset Cache: %s' % (e), log_utils.LOGWARNING) 42 | return False 43 | 44 | 45 | def _get_func(name, args=None, kwargs=None, cache_limit=1): 46 | if not cache_enabled: return False, None 47 | now = time.time() 48 | max_age = now - (cache_limit * 60 * 60) 49 | if args is None: args = [] 50 | if kwargs is None: kwargs = {} 51 | full_path = os.path.join(cache_path, _get_filename(name, args, kwargs)) 52 | if os.path.exists(full_path): 53 | mtime = os.path.getmtime(full_path) 54 | if mtime >= max_age: 55 | with open(full_path, 'rb') as f: 56 | pickled_result = f.read() 57 | # log_utils.log('Returning cached result: |%s|%s|%s| - modtime: %s max_age: %s age: %ss' % (name, args, kwargs, mtime, max_age, now - mtime), log_utils.LOGDEBUG) 58 | return True, pickle.loads(pickled_result) 59 | 60 | return False, None 61 | 62 | 63 | def _save_func(name, args=None, kwargs=None, result=None): 64 | try: 65 | if args is None: args = [] 66 | if kwargs is None: kwargs = {} 67 | pickled_result = pickle.dumps(result) 68 | full_path = os.path.join(cache_path, _get_filename(name, args, kwargs)) 69 | with open(full_path, 'wb') as f: 70 | f.write(pickled_result) 71 | except Exception as e: 72 | log_utils.log('Failure during cache write: %s' % (e), log_utils.LOGWARNING) 73 | 74 | 75 | def _get_filename(name, args, kwargs): 76 | arg_hash = hashlib.md5(name.encode('utf-8')).hexdigest() + hashlib.md5(str(args).encode('utf-8')).hexdigest() + hashlib.md5(str(kwargs).encode('utf-8')).hexdigest() 77 | return arg_hash 78 | 79 | 80 | def cache_method(cache_limit): 81 | def wrap(func): 82 | @functools.wraps(func) 83 | def memoizer(*args, **kwargs): 84 | if args: 85 | klass, real_args = args[0], args[1:] 86 | full_name = '%s.%s.%s' % (klass.__module__, klass.__class__.__name__, func.__name__) 87 | else: 88 | full_name = func.__name__ 89 | real_args = args 90 | in_cache, result = _get_func(full_name, real_args, kwargs, cache_limit=cache_limit) 91 | if in_cache: 92 | # log_utils.log('Using method cache for: |%s|%s|%s| -> |%d|' % (full_name, args, kwargs, len(pickle.dumps(result))), log_utils.LOGDEBUG) 93 | return result 94 | else: 95 | # log_utils.log('Calling cached method: |%s|%s|%s|' % (full_name, args, kwargs), log_utils.LOGDEBUG) 96 | result = func(*args, **kwargs) 97 | _save_func(full_name, real_args, kwargs, result) 98 | return result 99 | 100 | return memoizer 101 | 102 | return wrap 103 | 104 | 105 | # do not use this with instance methods the self parameter will cause args to never match 106 | def cache_function(cache_limit): 107 | def wrap(func): 108 | @functools.wraps(func) 109 | def memoizer(*args, **kwargs): 110 | name = func.__name__ 111 | in_cache, result = _get_func(name, args, kwargs, cache_limit=cache_limit) 112 | if in_cache: 113 | # log_utils.log('Using function cache for: |%s|%s|%s| -> |%d|' % (name, args, kwargs, len(pickle.dumps(result))), log_utils.LOGDEBUG) 114 | return result 115 | else: 116 | # log_utils.log('Calling cached function: |%s|%s|%s|' % (name, args, kwargs), log_utils.LOGDEBUG) 117 | result = func(*args, **kwargs) 118 | _save_func(name, args, kwargs, result) 119 | return result 120 | 121 | return memoizer 122 | 123 | return wrap 124 | -------------------------------------------------------------------------------- /resources/lib/addon_lib/constants.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2016-2019 anxdpanic 5 | 6 | This file is part of PlayThis (plugin.video.playthis) 7 | 8 | SPDX-License-Identifier: GPL-3.0-only 9 | See LICENSES/GPL-3.0-only for more information. 10 | """ 11 | 12 | import os 13 | 14 | from . import kodi 15 | from . import db_utils 16 | from .net import get_ua 17 | 18 | 19 | def __enum(**enums): 20 | return type('Enum', (), enums) 21 | 22 | 23 | DATABASE_VERSION = 1 24 | DATABASE_FILE = kodi.translate_path('special://database/{0!s}{1!s}.db'.format(kodi.get_name(), str(DATABASE_VERSION))) 25 | DATABASE = db_utils.SQLite(DATABASE_FILE) 26 | 27 | ADDON_DATA_DIR = kodi.translate_path('special://profile/addon_data/%s/' % kodi.get_id()) 28 | THUMBNAILS_DIR = kodi.translate_path('special://thumbnails/') 29 | 30 | COOKIE_FILE = kodi.translate_path('special://temp/%s/cookies.lwp' % kodi.get_id()) 31 | 32 | MODES = __enum( 33 | MAIN='main', 34 | PLAY='play', 35 | CLEARHISTORY='clearhistory', 36 | NEW='new', 37 | ADD='add', 38 | DELETE='delete', 39 | EXPORT_STRM='export_strm', 40 | EXPORT_M3U='export_m3u', 41 | SENDREMOTE='send_remote', 42 | RENAME='rename', 43 | CHANGETHUMB='changethumb', 44 | CLEARCACHE='clearcache', 45 | CLEARCOOKIES='clearcookies', 46 | YOUTUBEDL='ytdl', 47 | EXPORT_MENU='export_menu', 48 | MANAGE_MENU='manage_menu') 49 | 50 | ICONS = __enum( 51 | ADDON=kodi.translate_path('special://home/addons/{0!s}/icon.png'.format(kodi.get_id())), 52 | KODI=kodi.translate_path('special://xbmc/media/icon256x256.png'), 53 | YOUTUBEDL=kodi.translate_path('special://home/addons/script.module.youtube.dl/icon.png'), 54 | YOUTUBE=kodi.translate_path('special://home/addons/plugin.video.youtube/icon.png')) 55 | 56 | RAND_UA = get_ua() 57 | IE_USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; AS; rv:11.0) like Gecko' 58 | FF_USER_AGENT = 'Mozilla/5.0 (Windows NT 6.3; rv:36.0) Gecko/20100101 Firefox/36.0' 59 | OPERA_USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.111 Safari/537.36 OPR/34.0.2036.50' 60 | IOS_USER_AGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25' 61 | ANDROID_USER_AGENT = 'Mozilla/5.0 (Linux; Android 4.4.2; Nexus 4 Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.114 Mobile Safari/537.36' 62 | 63 | 64 | def _is_cookie_file(the_file): 65 | exists = os.path.exists(the_file) 66 | if not exists: 67 | return False 68 | else: 69 | try: 70 | tmp = kodi.vfs.File(the_file).read() 71 | if tmp.startswith('#LWP-Cookies-2.0'): 72 | return True 73 | return False 74 | except: 75 | with open(the_file, 'r') as f: 76 | tmp = f.readline() 77 | if tmp == '#LWP-Cookies-2.0\n': 78 | return True 79 | return False 80 | 81 | 82 | def _create_cookie(the_file): 83 | try: 84 | if kodi.vfs.exists(the_file): 85 | kodi.vfs.delete(the_file) 86 | _file = kodi.vfs.File(the_file, 'w') 87 | _file.write('#LWP-Cookies-2.0\n') 88 | _file.close() 89 | return the_file 90 | except: 91 | try: 92 | with open(the_file, 'w') as _file: 93 | _file.write('#LWP-Cookies-2.0\n') 94 | return the_file 95 | except: 96 | return '' 97 | 98 | 99 | if not _is_cookie_file(COOKIE_FILE): 100 | COOKIE_FILE = _create_cookie(COOKIE_FILE) 101 | -------------------------------------------------------------------------------- /resources/lib/addon_lib/db_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2016-2019 anxdpanic 5 | 6 | This file is part of PlayThis (plugin.video.playthis) 7 | 8 | SPDX-License-Identifier: GPL-3.0-only 9 | See LICENSES/GPL-3.0-only for more information. 10 | """ 11 | 12 | from sqlite3 import dbapi2 as sql 13 | 14 | from . import log_utils 15 | 16 | 17 | class SQLite: 18 | BEFORE_COMMIT_SQL = ['PRAG'] 19 | AFTER_COMMIT_SQL = ['VACU'] 20 | 21 | def __init__(self, db_file): 22 | """ 23 | :param db_file: str: containing path to sqlite database file 24 | """ 25 | self.db_file = db_file 26 | 27 | def __db_connect_(self): 28 | """ 29 | :return: instance: sqlite connection to db_file 30 | None: on error 31 | """ 32 | connection = None 33 | try: 34 | connection = sql.connect(self.db_file) 35 | connection.isolation_level = None 36 | except sql.Error as e: 37 | log_utils.log(str(e), log_utils.LOGERROR) 38 | connection = None 39 | finally: 40 | return connection 41 | 42 | def execute_w_rowcount(self, sql_statement, sql_params=None): 43 | """ 44 | wrapper for cursor.execute 45 | :param sql_statement: str: sql_statement may be parameterized (i. e. placeholders instead of SQL literals) 46 | :param sql_params: tuple, dict: sql_params supports two kinds of placeholders; 47 | tuple: question marks (qmark style) 48 | dict: named placeholders (named style). 49 | :return: tuple: int: 0: on error 50 | 1: sql_statement successfully executed, committed 51 | 2: duplicate record on insert 52 | int: rows affected 53 | 54 | """ 55 | if not sql_params: sql_params = '' 56 | connection = self.__db_connect_() 57 | if not connection: return 0 58 | connection.text_factory = str 59 | cursor = connection.cursor() 60 | result = 0 61 | rowcount = -1 62 | try: 63 | cursor.execute(sql_statement, sql_params) 64 | connection.commit() 65 | result = 1 66 | except sql.IntegrityError: 67 | result = 2 68 | except sql.Error as e: 69 | connection.rollback() 70 | log_utils.log(str(e), log_utils.LOGERROR) 71 | result = 0 72 | finally: 73 | rowcount = cursor.rowcount 74 | cursor.close() 75 | connection.close() 76 | return result, rowcount 77 | 78 | def execute(self, sql_statement, sql_params=None, suppress=False): 79 | """ 80 | wrapper for cursor.execute 81 | :param sql_statement: str: sql_statement may be parameterized (i. e. placeholders instead of SQL literals) 82 | :param sql_params: tuple, dict: sql_params supports two kinds of placeholders; 83 | tuple: question marks (qmark style) 84 | dict: named placeholders (named style). 85 | :param suppress: bool: suppress error log output 86 | 87 | :return: int: 0: on error 88 | 1: sql_statement successfully executed, committed 89 | 2: duplicate record on insert 90 | 91 | """ 92 | if not sql_params: sql_params = '' 93 | connection = self.__db_connect_() 94 | if not connection: return 0 95 | connection.text_factory = str 96 | cursor = connection.cursor() 97 | try: 98 | if sql_statement[:4] in self.BEFORE_COMMIT_SQL: 99 | cursor.execute(sql_statement, sql_params) 100 | if (sql_statement[:4] not in self.BEFORE_COMMIT_SQL) and (sql_statement[:4] not in self.AFTER_COMMIT_SQL): 101 | cursor.execute('BEGIN', '') 102 | cursor.execute(sql_statement, sql_params) 103 | cursor.execute('COMMIT', '') 104 | if sql_statement[:4] in self.AFTER_COMMIT_SQL: 105 | cursor.execute(sql_statement, sql_params) 106 | connection.commit() 107 | except sql.IntegrityError: 108 | return 2 109 | except sql.Error as e: 110 | connection.rollback() 111 | if not suppress: 112 | log_utils.log(str(e), log_utils.LOGERROR) 113 | return 0 114 | finally: 115 | cursor.close() 116 | connection.close() 117 | return 1 118 | 119 | def execute_many(self, sql_statements): 120 | """ 121 | wrapper for cursor.execute, list of statements in single transaction 122 | (performance increase over execute when multiple statements) 123 | :param sql_statements: list of [sql_statement, params] 124 | :param sql_statement: str: sql_statement may be parameterized (i. e. placeholders instead of SQL literals) 125 | :param sql_params: tuple, dict: sql_params supports two kinds of placeholders; 126 | tuple: question marks (qmark style) 127 | dict: named placeholders (named style). 128 | :return: int: 0: on error 129 | 1: sql_statement successfully executed, committed 130 | 2: duplicate record on insert 131 | 132 | """ 133 | connection = self.__db_connect_() 134 | if not connection: return 0 135 | connection.text_factory = str 136 | cursor = connection.cursor() 137 | try: 138 | cursor.execute('BEGIN', '') 139 | for sql_statement, sql_params in sql_statements: 140 | if not sql_params: sql_params = '' 141 | cursor.execute(sql_statement, sql_params) 142 | cursor.execute('COMMIT', '') 143 | connection.commit() 144 | except sql.IntegrityError: 145 | return 2 146 | except sql.Error as e: 147 | connection.rollback() 148 | log_utils.log(str(e), log_utils.LOGERROR) 149 | return 0 150 | finally: 151 | cursor.close() 152 | connection.close() 153 | return 1 154 | 155 | def fetch(self, sql_statement, sql_params=None): 156 | """ 157 | wrapper for cursor.fetchall 158 | :param sql_statement: str: sql_statement may be parameterized (i. e. placeholders instead of SQL literals) 159 | :param sql_params: tuple, dict: sql_params supports two kinds of placeholders; 160 | tuple: question marks (qmark style) 161 | dict: named placeholders (named style). 162 | :return: list of tuples: results of cursor.fetchall() 163 | None: on error 164 | """ 165 | if not sql_params: sql_params = '' 166 | connection = self.__db_connect_() 167 | if not connection: return None 168 | connection.text_factory = str 169 | cursor = connection.cursor() 170 | try: 171 | cursor.execute(sql_statement, sql_params) 172 | try: 173 | return cursor.fetchall() 174 | except: 175 | return cursor.fetchone() 176 | except sql.Error as e: 177 | log_utils.log(str(e), log_utils.LOGERROR) 178 | return None 179 | finally: 180 | cursor.close() 181 | connection.close() 182 | -------------------------------------------------------------------------------- /resources/lib/addon_lib/jsunpack.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Copyright (c) 2007-2013 Einar Lielmanis, Liam Newman, and contributors (JS Beautifier) 4 | Copyright (C) 2013-2016 Bstrdsmkr, t0mm0, tknorris (urlresolver XBMC Addon) 5 | Copyright (C) 2016-2019 anxdpanic 6 | 7 | This file is part of PlayThis (plugin.video.playthis) 8 | 9 | SPDX-License-Identifier: MIT 10 | See LICENSES/MIT for more information. 11 | 12 | Adapted for use in Kodi from: 13 | https://github.com/einars/js-beautify/blob/master/python/jsbeautifier/unpackers/packer.py 14 | 15 | Unpacker for Dean Edward's p.a.c.k.e.r, a part of javascript beautifier 16 | by Einar Lielmanis 17 | 18 | written by Stefano Sanfilippo 19 | 20 | usage: 21 | 22 | if detect(some_string): 23 | unpacked = unpack(some_string) 24 | 25 | Unpacker for Dean Edward's p.a.c.k.e.r 26 | """ 27 | 28 | import re 29 | 30 | 31 | def detect(source): 32 | """Detects whether `source` is P.A.C.K.E.R. coded.""" 33 | return source.replace(' ', '').startswith('eval(function(p,a,c,k,e,') 34 | 35 | 36 | def unpack(source): 37 | """Unpacks P.A.C.K.E.R. packed js code.""" 38 | payload, symtab, radix, count = _filterargs(source) 39 | 40 | if count != len(symtab): 41 | raise UnpackingError('Malformed p.a.c.k.e.r. symtab.') 42 | 43 | try: 44 | unbase = Unbaser(radix) 45 | except TypeError: 46 | raise UnpackingError('Unknown p.a.c.k.e.r. encoding.') 47 | 48 | def lookup(match): 49 | """Look up symbols in the synthetic symtab.""" 50 | word = match.group(0) 51 | return symtab[unbase(word)] or word 52 | 53 | source = re.sub(r'\b\w+\b', lookup, payload) 54 | return _replacestrings(source) 55 | 56 | 57 | def _filterargs(source): 58 | """Juice from a source file the four args needed by decoder.""" 59 | juicers = [(r"}\('(.*)', *(\d+), *(\d+), *'(.*)'\.split\('\|'\), *(\d+), *(.*)\)\)"), 60 | (r"}\('(.*)', *(\d+), *(\d+), *'(.*)'\.split\('\|'\)"), 61 | ] 62 | for juicer in juicers: 63 | args = re.search(juicer, source, re.DOTALL) 64 | if args: 65 | a = args.groups() 66 | try: 67 | return a[0], a[3].split('|'), int(a[1]), int(a[2]) 68 | except ValueError: 69 | raise UnpackingError('Corrupted p.a.c.k.e.r. data.') 70 | 71 | # could not find a satisfying regex 72 | raise UnpackingError('Could not make sense of p.a.c.k.e.r data (unexpected code structure)') 73 | 74 | 75 | def _replacestrings(source): 76 | """Strip string lookup table (list) and replace values in source.""" 77 | match = re.search(r'var *(_\w+)\=\["(.*?)"\];', source, re.DOTALL) 78 | 79 | if match: 80 | varname, strings = match.groups() 81 | startpoint = len(match.group(0)) 82 | lookup = strings.split('","') 83 | variable = '%s[%%d]' % varname 84 | for index, value in enumerate(lookup): 85 | source = source.replace(variable % index, '"%s"' % value) 86 | return source[startpoint:] 87 | return source 88 | 89 | 90 | class Unbaser(object): 91 | """Functor for a given base. Will efficiently convert 92 | strings to natural numbers.""" 93 | ALPHABET = { 94 | 56: '23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz', 95 | 59: '0123456789abcdefghijklmnopqrstuvwABCDEFGHIJKLMNOPQRSTUVWXYZ', 96 | 64: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/', 97 | 95: (' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ' 98 | '[\]^_`abcdefghijklmnopqrstuvwxyz{|}~') 99 | } 100 | 101 | def __init__(self, base): 102 | self.base = base 103 | 104 | # If base can be handled by int() builtin, let it do it for us 105 | if 2 <= base <= 36: 106 | self.unbase = lambda string: int(string, base) 107 | else: 108 | # Build conversion dictionary cache 109 | try: 110 | self.ALPHABET = self.ALPHABET[base] if base in self.ALPHABET else self.ALPHABET[64][0:base] 111 | self.dictionary = dict((cipher, index) for 112 | index, cipher in enumerate(self.ALPHABET)) 113 | except KeyError: 114 | raise TypeError('Unsupported base encoding.') 115 | 116 | self.unbase = self._dictunbaser 117 | 118 | def __call__(self, string): 119 | return self.unbase(string) 120 | 121 | def _dictunbaser(self, string): 122 | """Decodes a value to an integer.""" 123 | ret = 0 124 | for index, cipher in enumerate(string[::-1]): 125 | ret += (self.base ** index) * self.dictionary[cipher] 126 | return ret 127 | 128 | 129 | class UnpackingError(Exception): 130 | """Badly packed source or general error. Argument is a 131 | meaningful description.""" 132 | pass 133 | 134 | 135 | if __name__ == "__main__": 136 | # test = '''eval(function(p,a,c,k,e,d){while(c--)if(k[c])p=p.replace(new RegExp('\\b'+c.toString(a)+'\\b','g'),k[c]);return p}('4(\'30\').2z({2y:\'5://a.8.7/i/z/y/w.2x\',2w:{b:\'2v\',19:\'

<2 d="20" c="#17">2u 19.<16/><2 d="18" c="#15">2t 2s 2r 2q.

\',2p:\'

<2 d="20" c="#17">2o 2n b.<16/><2 d="18" c="#15">2m 2l 2k 2j.

\',},2i:\'2h\',2g:[{14:"11",b:"5://a.8.7/2f/13.12"},{14:"2e",b:"5://a.8.7/2d/13.12"},],2c:"11",2b:[{10:\'2a\',29:\'5://v.8.7/t-m/m.28\'},{10:\'27\'}],26:{\'25-3\':{\'24\':{\'23\':22,\'21\':\'5://a.8.7/i/z/y/\',\'1z\':\'w\',\'1y\':\'1x\'}}},s:\'5://v.8.7/t-m/s/1w.1v\',1u:"1t",1s:"1r",1q:\'1p\',1o:"1n",1m:"1l",1k:\'5\',1j:\'o\',});l e;l k=0;l 6=0;4().1i(9(x){f(6>0)k+=x.r-6;6=x.r;f(q!=0&&k>=q){6=-1;4().1h();4().1g(o);$(\'#1f\').j();$(\'h.g\').j()}});4().1e(9(x){6=-1});4().1d(9(x){n(x)});4().1c(9(){$(\'h.g\').j()});9 n(x){$(\'h.g\').1b();f(e)1a;e=1;}',36,109,'||font||jwplayer|http|p0102895|me|vidto|function|edge3|file|color|size|vvplay|if|video_ad|div||show|tt102895|var|player|doPlay|false||21600|position|skin|test||static|1y7okrqkv4ji||00020|01|type|360p|mp4|video|label|FFFFFF|br|FF0000||deleted|return|hide|onComplete|onPlay|onSeek|play_limit_box|setFullscreen|stop|onTime|dock|provider|391|height|650|width|over|controlbar|5110|duration|uniform|stretching|zip|stormtrooper|213|frequency|prefix||path|true|enabled|preview|timeslidertooltipplugin|plugins|html5|swf|src|flash|modes|hd_default|3bjhohfxpiqwws4phvqtsnolxocychumk274dsnkblz6sfgq6uz6zt77gxia|240p|3bjhohfxpiqwws4phvqtsnolxocychumk274dsnkba36sfgq6uzy3tv2oidq|hd|original|ratio|broken|is|link|Your|such|No|nofile|more|any|availabe|Not|File|OK|previw|jpg|image|setup|flvplayer'.split('|')))''' 137 | # test = '''eval(function(p,a,c,k,e,d){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('y.x(A(\'%0%f%b%9%1%d%8%8%o%e%B%c%0%e%d%0%f%w%1%7%3%2%p%d%1%n%2%1%c%0%t%0%f%7%8%8%d%5%6%1%7%e%b%l%7%1%2%e%9%q%c%0%6%1%z%2%0%f%b%1%9%c%0%s%6%6%l%G%4%4%5%5%5%k%b%7%5%8%o%i%2%k%6%i%4%2%3%p%2%n%4%5%7%6%9%s%4%j%q%a%h%a%3%a%E%a%3%D%H%9%K%C%I%m%r%g%h%L%v%g%u%F%r%g%3%J%3%j%3%m%h%4\'));',48,48,'22|72|65|6d|2f|77|74|61|6c|63|4e|73|3d|6f|6e|20|4d|32|76|59|2e|70|51|64|69|62|79|31|68|30|7a|34|66|write|document|75|unescape|67|4f|5a|57|55|3a|44|47|4a|78|49'.split('|'),0,{}))''' 138 | # test = '''eval(function(p,a,c,k,e,d){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('x.w(z(\'%1%f%9%b%0%d%7%7%m%e%A%c%1%e%d%1%f%v%0%3%i%2%o%d%0%s%2%0%c%1%q%1%f%3%7%7%d%6%5%0%3%e%9%l%3%0%2%e%b%g%c%1%5%0%y%2%1%f%9%0%b%c%1%r%5%5%l%E%4%4%6%6%6%n%9%3%6%7%m%k%2%n%5%k%4%2%i%o%2%s%4%6%3%5%b%r%4%8%D%h%C%a%F%8%H%B%I%h%i%a%g%8%u%a%q%j%t%j%g%8%t%h%p%j%p%a%G%4\'));',45,45,'72|22|65|61|2f|74|77|6c|5a|73|55|63|3d|6f|6e|20|79|59|6d|4d|76|70|69|2e|62|7a|30|68|64|44|54|66|write|document|75|unescape|67|51|32|6a|3a|35|5f|47|34'.split('|'),0,{}))''' 139 | test = '''eval(function(p,a,c,k,e,d){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('q.r(s(\'%h%t%a%p%u%6%c%n%0%5%l%4%2%4%7%j%0%8%1%o%b%3%7%m%1%8%a%7%b%3%d%6%1%f%0%v%1%5%D%9%0%5%c%g%0%4%A%9%0%f%k%z%2%8%1%C%2%i%d%6%2%3%k%j%2%3%y%e%x%w%g%B%E%F%i%h%e\'));',42,42,'5a|4d|4f|54|6a|44|33|6b|57|7a|56|4e|68|55|3e|47|69|65|6d|32|45|46|31|6f|30|75|document|write|unescape|6e|62|6c|2f|3c|22|79|63|66|78|59|72|61'.split('|'),0,{}))''' 140 | print(unpack(test)) 141 | -------------------------------------------------------------------------------- /resources/lib/addon_lib/kodi.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2015-2016 tknorris 5 | Copyright (C) 2016-2019 anxdpanic 6 | 7 | This file is part of PlayThis (plugin.video.playthis) 8 | 9 | SPDX-License-Identifier: GPL-3.0-only 10 | See LICENSES/GPL-3.0-only for more information. 11 | """ 12 | 13 | import sys 14 | import os 15 | import re 16 | import json 17 | import time 18 | 19 | from six import PY2 20 | from six import PY3 21 | from six import string_types 22 | from six import text_type 23 | from six import with_metaclass 24 | from six.moves.urllib_parse import parse_qs 25 | from six.moves.urllib_parse import urlencode 26 | 27 | import xbmcaddon 28 | import xbmcplugin 29 | import xbmcgui 30 | import xbmc 31 | import xbmcvfs 32 | 33 | from . import strings 34 | 35 | try: 36 | xbmc.translatePath = xbmcvfs.translatePath 37 | except AttributeError: 38 | pass 39 | 40 | __log = xbmc.log 41 | 42 | Addon = xbmcaddon.Addon 43 | Dialog = xbmcgui.Dialog 44 | Monitor = xbmc.Monitor 45 | Player = xbmc.Player 46 | execute_builtin = xbmc.executebuiltin 47 | sleep = xbmc.sleep 48 | conditional_visibility = xbmc.getCondVisibility 49 | getCurrentWindowDialogId = xbmcgui.getCurrentWindowDialogId 50 | get_supported_media = xbmc.getSupportedMedia 51 | vfs = xbmcvfs 52 | 53 | addon = xbmcaddon.Addon() 54 | get_setting = addon.getSetting 55 | show_settings = addon.openSettings 56 | 57 | 58 | def decode_utf8(string, ignore=False): 59 | try: 60 | if not ignore: 61 | return string.decode('utf-8') 62 | else: 63 | return string.decode('utf-8', 'ignore') 64 | except AttributeError: 65 | return string 66 | 67 | 68 | def is_unicode(string): 69 | return PY2 and isinstance(string, text_type) 70 | 71 | 72 | def execute_jsonrpc(command): 73 | if not isinstance(command, string_types): 74 | command = json.dumps(command) 75 | response = xbmc.executeJSONRPC(command) 76 | return json.loads(response) 77 | 78 | 79 | def get_handle(): 80 | return int(sys.argv[1]) 81 | 82 | 83 | def get_path(): 84 | return decode_utf8(addon.getAddonInfo('path')) 85 | 86 | 87 | def get_profile(): 88 | return decode_utf8(addon.getAddonInfo('profile')) 89 | 90 | 91 | def translate_path(path): 92 | return decode_utf8(xbmc.translatePath(path)) 93 | 94 | 95 | def set_setting(id, value): 96 | if not isinstance(value, string_types): value = str(value) 97 | addon.setSetting(id, value) 98 | 99 | 100 | def get_version(): 101 | return addon.getAddonInfo('version') 102 | 103 | 104 | def get_id(): 105 | return addon.getAddonInfo('id') 106 | 107 | 108 | def get_name(): 109 | return addon.getAddonInfo('name') 110 | 111 | 112 | def get_icon(): 113 | return os.path.join(get_path(), 'icon.png') 114 | 115 | 116 | def get_fanart(): 117 | return os.path.join(get_path(), 'fanart.jpg') 118 | 119 | 120 | def get_plugin_url(queries): 121 | try: 122 | query = urlencode(queries) 123 | except UnicodeEncodeError: 124 | for k in queries: 125 | if is_unicode(queries[k]): 126 | queries[k] = queries[k].encode('utf-8') 127 | query = urlencode(queries) 128 | 129 | return sys.argv[0] + '?' + query 130 | 131 | 132 | def end_of_directory(cache_to_disc=True): 133 | xbmcplugin.endOfDirectory(int(sys.argv[1]), cacheToDisc=cache_to_disc) 134 | 135 | 136 | def set_content(content): 137 | xbmcplugin.setContent(int(sys.argv[1]), content) 138 | 139 | 140 | def create_item(queries, label, thumb='', fanart='', is_folder=None, is_playable=None, total_items=0, menu_items=None, 141 | replace_menu=False, content_type='video', info=None): 142 | list_item = ListItem(label) 143 | add_item(queries, list_item, thumb, fanart, is_folder, is_playable, total_items, 144 | menu_items, replace_menu, content_type=content_type, info=info) 145 | 146 | 147 | def add_item(queries, list_item, thumb='', fanart='', is_folder=None, is_playable=None, total_items=0, menu_items=None, 148 | replace_menu=False, content_type='video', info=None): 149 | if menu_items is None: menu_items = [] 150 | 151 | if info is None: info = {'title': list_item.getLabel()} 152 | if not thumb: thumb = get_icon() 153 | if not fanart: fanart = get_fanart() 154 | 155 | if is_folder is None: 156 | is_folder = False if is_playable else True 157 | 158 | if is_playable is None: 159 | playable = 'false' if is_folder else 'true' 160 | else: 161 | playable = 'true' if is_playable else 'false' 162 | 163 | liz_url = queries 164 | if isinstance(queries, dict): 165 | liz_url = get_plugin_url(queries) 166 | 167 | list_item.setArt({'icon': thumb, 'thumb': thumb, 'fanart': fanart}) 168 | list_item.setInfo(content_type, info) 169 | list_item.setProperty('isPlayable', playable) 170 | list_item.addContextMenuItems(menu_items, replaceItems=replace_menu) 171 | xbmcplugin.addDirectoryItem(int(sys.argv[1]), liz_url, list_item, isFolder=is_folder, totalItems=total_items) 172 | 173 | 174 | def get_playlist(list_type, new=False): 175 | # 0 = music, # 1 = video 176 | pl = xbmc.PlayList(list_type) 177 | if new: 178 | pl.clear() 179 | return pl 180 | 181 | 182 | def parse_query(query): 183 | q = {'mode': 'main'} 184 | if query.startswith('?'): query = query[1:] 185 | queries = parse_qs(query) 186 | for key in queries: 187 | if len(queries[key]) == 1: 188 | q[key] = queries[key][0] 189 | else: 190 | q[key] = queries[key] 191 | return q 192 | 193 | 194 | def notify(header=None, msg='', duration=2000, sound=None): 195 | if header is None: header = get_name() 196 | if sound is None: sound = get_setting('mute_notifications') == 'false' 197 | icon_path = os.path.join(get_path(), 'icon.png') 198 | try: 199 | xbmcgui.Dialog().notification(header, msg, icon_path, duration, sound) 200 | except: 201 | builtin = "Notification(%s, %s, %s, %s)" % (header, msg, duration, icon_path) 202 | xbmc.executebuiltin(builtin) 203 | 204 | 205 | def set_resolved_url(list_item, resolved=True): 206 | xbmcplugin.setResolvedUrl(get_handle(), resolved, list_item) 207 | 208 | 209 | def get_info_label(name): 210 | return xbmc.getInfoLabel('%s') % name 211 | 212 | 213 | def get_current_view(): 214 | skinPath = translate_path('special://skin/') 215 | xml = os.path.join(skinPath, 'addon.xml') 216 | f = xbmcvfs.File(xml) 217 | read = f.read() 218 | f.close() 219 | try: 220 | src = re.search('defaultresolution="([^"]+)', read, re.DOTALL).group(1) 221 | except: 222 | src = re.search('([^<]+)', read, re.DOTALL) 228 | if match: 229 | views = match.group(1) 230 | for view in views.split(','): 231 | if xbmc.getInfoLabel('Control.GetLabel(%s)' % view): return view 232 | 233 | 234 | def refresh_container(): 235 | xbmc.executebuiltin("Container.Refresh") 236 | 237 | 238 | def update_container(url): 239 | xbmc.executebuiltin('Container.Update(%s)' % (url)) 240 | 241 | 242 | def get_keyboard(heading, default=''): 243 | keyboard = xbmc.Keyboard() 244 | keyboard.setHeading(heading) 245 | if default: keyboard.setDefault(default) 246 | keyboard.doModal() 247 | if keyboard.isConfirmed(): 248 | return keyboard.getText().strip() 249 | else: 250 | return None 251 | 252 | 253 | def i18n(string_id): 254 | try: 255 | if PY3: 256 | return addon.getLocalizedString(strings.STRINGS[string_id]) 257 | else: 258 | return addon.getLocalizedString(strings.STRINGS[string_id]).encode('utf-8', 'ignore') 259 | except Exception as e: 260 | xbmc.log('%s: Failed String Lookup: %s (%s)' % (get_name(), string_id, e), xbmc.LOGWARNING) 261 | return string_id 262 | 263 | 264 | def string_to_filename(string): 265 | filename = string.strip() 266 | filename = filename.replace(' ', '_') 267 | filename = ''.join(c for c in filename if c.isalnum() or c in ('.', '_')) 268 | filename = re.sub('_{2,}', '_', filename) 269 | return filename 270 | 271 | 272 | def loose_version(v): 273 | filled = [] 274 | for point in v.split("."): 275 | filled.append(point.zfill(8)) 276 | return tuple(filled) 277 | 278 | 279 | def has_addon(addon_id): 280 | return xbmc.getCondVisibility('System.HasAddon(%s)' % addon_id) == 1 281 | 282 | 283 | def addon_enabled(addon_id): 284 | rpc_request = {"jsonrpc": "2.0", 285 | "method": "Addons.GetAddonDetails", 286 | "id": 1, 287 | "params": {"addonid": "%s" % addon_id, 288 | "properties": ["enabled"]} 289 | } 290 | response = execute_jsonrpc(rpc_request) 291 | try: 292 | return response['result']['addon']['enabled'] is True 293 | except KeyError: 294 | message = response['error']['message'] 295 | code = response['error']['code'] 296 | error = 'Requested |%s| received error |%s| and code: |%s|' % (rpc_request, message, code) 297 | xbmc.log(error, xbmc.LOGINFO) 298 | return False 299 | 300 | 301 | def set_addon_enabled(addon_id, enabled=True): 302 | rpc_request = {"jsonrpc": "2.0", 303 | "method": "Addons.SetAddonEnabled", 304 | "id": 1, 305 | "params": {"addonid": "%s" % addon_id, 306 | "enabled": enabled} 307 | } 308 | response = execute_jsonrpc(rpc_request) 309 | try: 310 | return response['result'] == 'OK' 311 | except KeyError: 312 | message = response['error']['message'] 313 | code = response['error']['code'] 314 | error = 'Requested |%s| received error |%s| and code: |%s|' % (rpc_request, message, code) 315 | xbmc.log(error, xbmc.LOGINFO) 316 | return False 317 | 318 | 319 | def stop_player(player_id=None): 320 | # 0 = audio, 1 = video, 2 = image, None = Active 321 | if player_id is None: 322 | rpc_request = {"id": 1, "jsonrpc": "2.0", "method": "Player.GetActivePlayers"} 323 | response = execute_jsonrpc(rpc_request) 324 | try: 325 | player_id = response['result'][0]['playerid'] 326 | except IndexError: # player not running or already stopped 327 | return True 328 | rpc_request = {"id": 1, "jsonrpc": "2.0", "method": "Player.Stop", "params": {"playerid": player_id}} 329 | response = execute_jsonrpc(rpc_request) 330 | try: 331 | return response['result'] == 'OK' 332 | except KeyError: 333 | message = response['error']['message'] 334 | code = response['error']['code'] 335 | error = 'Requested |%s| received error |%s| and code: |%s|' % (rpc_request, message, code) 336 | xbmc.log(error, xbmc.LOGINFO) 337 | return False 338 | 339 | 340 | def close_dialog(dialog_name, forced=True): 341 | if not forced: 342 | forced = 'false' 343 | else: 344 | forced = 'true' 345 | xbmc.executebuiltin('Dialog.Close(%s,%s)' % (dialog_name, forced)) 346 | 347 | 348 | def get_kodi_version(): 349 | class MetaClass(type): 350 | def __str__(self): 351 | return '|%s| -> |%s|%s|%s|%s|%s|' % (self.version, self.major, self.minor, self.tag, self.tag_version, self.revision) 352 | 353 | class KodiVersion(with_metaclass(MetaClass, object)): 354 | version = decode_utf8(xbmc.getInfoLabel('System.BuildVersion')) 355 | match = re.search('([0-9]+)\.([0-9]+)', version) 356 | if match: major, minor = match.groups() 357 | match = re.search('-([a-zA-Z]+)([0-9]*)', version) 358 | if match: tag, tag_version = match.groups() 359 | match = re.search('\w+:(\w+-\w+)', version) 360 | if match: revision = match.group(1) 361 | 362 | try: 363 | major = int(major) 364 | except: 365 | major = 0 366 | try: 367 | minor = int(minor) 368 | except: 369 | minor = 0 370 | try: 371 | revision = decode_utf8(revision) 372 | except: 373 | revision = u'' 374 | try: 375 | tag = decode_utf8(tag) 376 | except: 377 | tag = u'' 378 | try: 379 | tag_version = int(tag_version) 380 | except: 381 | tag_version = 0 382 | 383 | return KodiVersion 384 | 385 | 386 | class WorkingDialog(object): 387 | wd = None 388 | kv = get_kodi_version().major 389 | 390 | def __init__(self): 391 | try: 392 | if self.kv < 18: 393 | self.wd = xbmcgui.DialogBusy() 394 | self.wd.create() 395 | self.update(0) 396 | else: 397 | xbmc.executebuiltin('ActivateWindow(busydialognocancel)') 398 | except: 399 | if self.kv < 18: 400 | xbmc.executebuiltin('ActivateWindow(busydialog)') 401 | else: 402 | xbmc.executebuiltin('ActivateWindow(busydialognocancel)') 403 | 404 | def __enter__(self): 405 | return self 406 | 407 | def __exit__(self, type, value, traceback): 408 | if self.wd is not None: 409 | self.wd.close() 410 | else: 411 | if self.kv < 18: 412 | xbmc.executebuiltin('Dialog.Close(busydialog)') 413 | else: 414 | xbmc.executebuiltin('Dialog.Close(busydialognocancel)') 415 | 416 | def is_canceled(self): 417 | if self.wd is not None: 418 | return self.wd.iscanceled() 419 | else: 420 | return False 421 | 422 | def update(self, percent): 423 | if self.wd is not None: 424 | self.wd.update(percent) 425 | 426 | 427 | class ProgressDialog(object): 428 | pd = None 429 | 430 | def __init__(self, heading, line1='', line2='', line3='', background=False, active=True, timer=0): 431 | self.begin = time.time() 432 | self.timer = timer 433 | self.background = background 434 | self.heading = heading 435 | 436 | if active and not timer: 437 | self.pd = self.__create_dialog(line1, line2, line3) 438 | self.pd.update(0) 439 | 440 | def __create_dialog(self, line1, line2, line3): 441 | if self.background: 442 | pd = xbmcgui.DialogProgressBG() 443 | msg = line1 + line2 + line3 444 | pd.create(self.heading, msg) 445 | else: 446 | pd = xbmcgui.DialogProgress() 447 | if get_kodi_version().major <= 18: 448 | pd.create(self.heading, line1, line2, line3) 449 | else: 450 | msg = '[CR]'.join([line1, line2, line3]) 451 | pd.create(self.heading, msg) 452 | return pd 453 | 454 | def __enter__(self): 455 | return self 456 | 457 | def __exit__(self, type, value, traceback): 458 | if self.pd is not None: 459 | self.pd.close() 460 | 461 | def is_canceled(self): 462 | if self.pd is not None and not self.background: 463 | return self.pd.iscanceled() 464 | else: 465 | return False 466 | 467 | def update(self, percent, line1='', line2='', line3=''): 468 | if self.pd is None and self.timer and (time.time() - self.begin) >= self.timer: 469 | self.pd = self.__create_dialog(line1, line2, line3) 470 | 471 | if self.pd is not None: 472 | if self.background: 473 | msg = line1 + line2 + line3 474 | self.pd.update(percent, self.heading, msg) 475 | else: 476 | if get_kodi_version().major <= 18: 477 | self.pd.update(percent, line1, line2, line3) 478 | else: 479 | msg = '[CR]'.join([line1, line2, line3]) 480 | self.pd.update(percent, msg) 481 | 482 | 483 | class CountdownDialog(object): 484 | __INTERVALS = 5 485 | pd = None 486 | 487 | def __init__(self, heading, line1='', line2='', line3='', active=True, countdown=60, interval=5): 488 | self.heading = heading 489 | self.countdown = countdown 490 | self.interval = interval 491 | self.line3 = line3 492 | if active: 493 | pd = xbmcgui.DialogProgress() 494 | if not self.line3: line3 = 'Expires in: %s seconds' % (countdown) 495 | pd.create(self.heading, line1, line2, line3) 496 | pd.update(100) 497 | self.pd = pd 498 | 499 | def __enter__(self): 500 | return self 501 | 502 | def __exit__(self, type, value, traceback): 503 | if self.pd is not None: 504 | self.pd.close() 505 | 506 | def start(self, func, args=None, kwargs=None): 507 | if args is None: args = [] 508 | if kwargs is None: kwargs = {} 509 | result = func(*args, **kwargs) 510 | if result: 511 | return result 512 | 513 | start = time.time() 514 | expires = time_left = int(self.countdown) 515 | interval = self.interval 516 | while time_left > 0: 517 | for _ in range(CountdownDialog.__INTERVALS): 518 | sleep(interval * 1000 / CountdownDialog.__INTERVALS) 519 | if self.is_canceled(): return 520 | time_left = expires - int(time.time() - start) 521 | if time_left < 0: time_left = 0 522 | progress = time_left * 100 / expires 523 | line3 = 'Expires in: %s seconds' % (time_left) if not self.line3 else '' 524 | self.update(progress, line3=line3) 525 | 526 | result = func(*args, **kwargs) 527 | if result: 528 | return result 529 | 530 | def is_canceled(self): 531 | if self.pd is None: 532 | return False 533 | else: 534 | return self.pd.iscanceled() 535 | 536 | def update(self, percent, line1='', line2='', line3=''): 537 | if self.pd is not None: 538 | self.pd.update(percent, line1, line2, line3) 539 | 540 | 541 | class ListItem(xbmcgui.ListItem): 542 | def setArt(self, dictionary): 543 | if get_kodi_version().major < 16 and 'icon' in dictionary: 544 | self.setIconImage(dictionary['icon']) 545 | del dictionary['icon'] 546 | super(ListItem, self).setArt(dictionary) 547 | -------------------------------------------------------------------------------- /resources/lib/addon_lib/log_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2015-2016 tknorris 5 | Copyright (C) 2016-2019 anxdpanic 6 | 7 | This file is part of PlayThis (plugin.video.playthis) 8 | 9 | SPDX-License-Identifier: GPL-3.0-only 10 | See LICENSES/GPL-3.0-only for more information. 11 | """ 12 | 13 | import time 14 | 15 | from xbmc import LOGDEBUG 16 | from xbmc import LOGERROR 17 | from xbmc import LOGFATAL 18 | from xbmc import LOGINFO 19 | from xbmc import LOGNONE 20 | from xbmc import LOGWARNING 21 | 22 | LOGNOTICE = LOGINFO 23 | 24 | from . import kodi 25 | 26 | 27 | __all__ = ['log', 'trace', 'LOGDEBUG', 'LOGERROR', 'LOGFATAL', 'LOGINFO', 'LOGNONE', 'LOGNOTICE', 'LOGWARNING'] 28 | 29 | 30 | name = kodi.get_name() 31 | 32 | 33 | def log(msg, level=LOGDEBUG): 34 | try: 35 | if kodi.is_unicode(msg): 36 | msg = '%s (ENCODED)' % msg.encode('utf-8') 37 | 38 | kodi.__log('%s: %s' % (name, msg), level) 39 | except Exception as e: 40 | try: 41 | kodi.__log('Logging Failure: %s' % (e), level) 42 | except: 43 | pass 44 | 45 | 46 | def trace(method): 47 | # @trace decorator 48 | def method_trace_on(*args, **kwargs): 49 | start = time.time() 50 | result = method(*args, **kwargs) 51 | end = time.time() 52 | log('{name!r} time: {time:2.4f}s args: |{args!r}| kwargs: |{kwargs!r}|' 53 | .format(name=method.__name__,time=end - start, args=args, kwargs=kwargs), LOGDEBUG) 54 | return result 55 | 56 | def method_trace_off(*args, **kwargs): 57 | return method(*args, **kwargs) 58 | 59 | if __is_debugging(): 60 | return method_trace_on 61 | else: 62 | return method_trace_off 63 | 64 | 65 | def __is_debugging(): 66 | command = {'jsonrpc': '2.0', 'id': 1, 'method': 'Settings.getSettings', 67 | 'params': {'filter': {'section': 'system', 'category': 'logging'}}} 68 | js_data = kodi.execute_jsonrpc(command) 69 | if 'result' in js_data and 'settings' in js_data['result']: 70 | for item in js_data['result']['settings']: 71 | if item['id'] == 'debug.showloginfo': 72 | return item['value'] 73 | 74 | return False 75 | -------------------------------------------------------------------------------- /resources/lib/addon_lib/net.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2011-2016 t0mm0 (common XBMC Module) 5 | Copyright (C) 2016-2019 anxdpanic 6 | 7 | This file is part of PlayThis (plugin.video.playthis) 8 | 9 | SPDX-License-Identifier: GPL-3.0-only 10 | See LICENSES/GPL-3.0-only for more information. 11 | """ 12 | 13 | import random 14 | import gzip 15 | import re 16 | import socket 17 | import time 18 | 19 | from six import StringIO 20 | from six import string_types 21 | from six.moves import http_cookiejar as cookielib 22 | from six.moves.urllib_parse import urlencode 23 | from six.moves import urllib_request 24 | from six.moves import xrange 25 | 26 | from . import kodi 27 | 28 | 29 | # Set Global timeout - Useful for slow connections and Putlocker. 30 | socket.setdefaulttimeout(10) 31 | 32 | BR_VERS = [ 33 | ['%s.0' % i for i in xrange(18, 50)], 34 | ['37.0.2062.103', '37.0.2062.120', '37.0.2062.124', '38.0.2125.101', '38.0.2125.104', '38.0.2125.111', '39.0.2171.71', '39.0.2171.95', '39.0.2171.99', '40.0.2214.93', '40.0.2214.111', 35 | '40.0.2214.115', '42.0.2311.90', '42.0.2311.135', '42.0.2311.152', '43.0.2357.81', '43.0.2357.124', '44.0.2403.155', '44.0.2403.157', '45.0.2454.101', '45.0.2454.85', '46.0.2490.71', 36 | '46.0.2490.80', '46.0.2490.86', '47.0.2526.73', '47.0.2526.80', '48.0.2564.116', '49.0.2623.112', '50.0.2661.86'], 37 | ['11.0'], 38 | ['8.0', '9.0', '10.0', '10.6']] 39 | WIN_VERS = ['Windows NT 10.0', 'Windows NT 7.0', 'Windows NT 6.3', 'Windows NT 6.2', 'Windows NT 6.1', 'Windows NT 6.0', 'Windows NT 5.1', 'Windows NT 5.0'] 40 | FEATURES = ['; WOW64', '; Win64; IA64', '; Win64; x64', ''] 41 | RAND_UAS = ['Mozilla/5.0 ({win_ver}{feature}; rv:{br_ver}) Gecko/20100101 Firefox/{br_ver}', 42 | 'Mozilla/5.0 ({win_ver}{feature}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{br_ver} Safari/537.36', 43 | 'Mozilla/5.0 ({win_ver}{feature}; Trident/7.0; rv:{br_ver}) like Gecko', 44 | 'Mozilla/5.0 (compatible; MSIE {br_ver}; {win_ver}{feature}; Trident/6.0)'] 45 | 46 | 47 | def get_ua(): 48 | try: 49 | last_gen = int(kodi.get_setting('last_ua_create')) 50 | except: 51 | last_gen = 0 52 | if not kodi.get_setting('current_ua') or last_gen < (time.time() - (7 * 24 * 60 * 60)): 53 | index = random.randrange(len(RAND_UAS)) 54 | versions = {'win_ver': random.choice(WIN_VERS), 'feature': random.choice(FEATURES), 'br_ver': random.choice(BR_VERS[index])} 55 | user_agent = RAND_UAS[index].format(**versions) 56 | 57 | kodi.set_setting('current_ua', user_agent) 58 | kodi.set_setting('last_ua_create', str(int(time.time()))) 59 | else: 60 | user_agent = kodi.get_setting('current_ua') 61 | return user_agent 62 | 63 | 64 | class Net: 65 | ''' 66 | This class wraps :mod:`urllib2` and provides an easy way to make http 67 | requests while taking care of cookies, proxies, gzip compression and 68 | character encoding. 69 | 70 | Example:: 71 | 72 | from addon.common.net import Net 73 | net = Net() 74 | response = net.http_GET('http://xbmc.org') 75 | print response.content 76 | ''' 77 | 78 | _cj = cookielib.LWPCookieJar() 79 | _proxy = None 80 | _user_agent = 'Mozilla/5.0 (Windows NT 6.3; rv:36.0) Gecko/20100101 Firefox/36.0' 81 | _http_debug = False 82 | 83 | def __init__(self, cookie_file='', proxy='', user_agent='', http_debug=False): 84 | ''' 85 | Kwargs: 86 | cookie_file (str): Full path to a file to be used to load and save 87 | cookies to. 88 | 89 | proxy (str): Proxy setting (eg. 90 | ``'http://user:pass@example.com:1234'``) 91 | 92 | user_agent (str): String to use as the User Agent header. If not 93 | supplied the class will use a default user agent (chrome) 94 | 95 | http_debug (bool): Set ``True`` to have HTTP header info written to 96 | the XBMC log for all requests. 97 | ''' 98 | if cookie_file: 99 | self.set_cookies(cookie_file) 100 | if proxy: 101 | self.set_proxy(proxy) 102 | if user_agent: 103 | self.set_user_agent(user_agent) 104 | self._http_debug = http_debug 105 | self._update_opener() 106 | 107 | def set_cookies(self, cookie_file): 108 | ''' 109 | Set the cookie file and try to load cookies from it if it exists. 110 | 111 | Args: 112 | cookie_file (str): Full path to a file to be used to load and save 113 | cookies to. 114 | ''' 115 | try: 116 | self._cj.load(cookie_file, ignore_discard=True) 117 | self._update_opener() 118 | return True 119 | except: 120 | return False 121 | 122 | def get_cookies(self, as_dict=False): 123 | '''Returns A dictionary containing all cookie information by domain.''' 124 | if as_dict: 125 | return dict((cookie.name, cookie.value) for cookie in self._cj) 126 | else: 127 | return self._cj._cookies 128 | 129 | def save_cookies(self, cookie_file): 130 | ''' 131 | Saves cookies to a file. 132 | 133 | Args: 134 | cookie_file (str): Full path to a file to save cookies to. 135 | ''' 136 | self._cj.save(cookie_file, ignore_discard=True) 137 | 138 | def set_proxy(self, proxy): 139 | ''' 140 | Args: 141 | proxy (str): Proxy setting (eg. 142 | ``'http://user:pass@example.com:1234'``) 143 | ''' 144 | self._proxy = proxy 145 | self._update_opener() 146 | 147 | def get_proxy(self): 148 | '''Returns string containing proxy details.''' 149 | return self._proxy 150 | 151 | def set_user_agent(self, user_agent): 152 | ''' 153 | Args: 154 | user_agent (str): String to use as the User Agent header. 155 | ''' 156 | self._user_agent = user_agent 157 | 158 | def get_user_agent(self): 159 | '''Returns user agent string.''' 160 | return self._user_agent 161 | 162 | def _update_opener(self): 163 | ''' 164 | Builds and installs a new opener to be used by all future calls to 165 | :func:`urllib2.urlopen`. 166 | ''' 167 | if self._http_debug: 168 | http = urllib_request.HTTPHandler(debuglevel=1) 169 | else: 170 | http = urllib_request.HTTPHandler() 171 | 172 | if self._proxy: 173 | opener = urllib_request.build_opener(urllib_request.HTTPCookieProcessor(self._cj), 174 | urllib_request.ProxyHandler({'http': self._proxy}), 175 | urllib_request.HTTPBasicAuthHandler(), http) 176 | else: 177 | opener = urllib_request.build_opener(urllib_request.HTTPCookieProcessor(self._cj), 178 | urllib_request.HTTPBasicAuthHandler(), http) 179 | urllib_request.install_opener(opener) 180 | 181 | def http_GET(self, url, headers={}, compression=True): 182 | ''' 183 | Perform an HTTP GET request. 184 | 185 | Args: 186 | url (str): The URL to GET. 187 | 188 | Kwargs: 189 | headers (dict): A dictionary describing any headers you would like 190 | to add to the request. (eg. ``{'X-Test': 'testing'}``) 191 | 192 | compression (bool): If ``True`` (default), try to use gzip 193 | compression. 194 | 195 | Returns: 196 | An :class:`HttpResponse` object containing headers and other 197 | meta-information about the page and the page content. 198 | ''' 199 | return self._fetch(url, headers=headers, compression=compression) 200 | 201 | def http_POST(self, url, form_data, headers={}, compression=True): 202 | ''' 203 | Perform an HTTP POST request. 204 | 205 | Args: 206 | url (str): The URL to POST. 207 | 208 | form_data (dict): A dictionary of form data to POST. 209 | 210 | Kwargs: 211 | headers (dict): A dictionary describing any headers you would like 212 | to add to the request. (eg. ``{'X-Test': 'testing'}``) 213 | 214 | compression (bool): If ``True`` (default), try to use gzip 215 | compression. 216 | 217 | Returns: 218 | An :class:`HttpResponse` object containing headers and other 219 | meta-information about the page and the page content. 220 | ''' 221 | return self._fetch(url, form_data, headers=headers, compression=compression) 222 | 223 | def http_HEAD(self, url, headers={}): 224 | ''' 225 | Perform an HTTP HEAD request. 226 | 227 | Args: 228 | url (str): The URL to GET. 229 | 230 | Kwargs: 231 | headers (dict): A dictionary describing any headers you would like 232 | to add to the request. (eg. ``{'X-Test': 'testing'}``) 233 | 234 | Returns: 235 | An :class:`HttpResponse` object containing headers and other 236 | meta-information about the page. 237 | ''' 238 | request = urllib_request.Request(url) 239 | request.get_method = lambda: 'HEAD' 240 | request.add_header('User-Agent', self._user_agent) 241 | for key in headers: 242 | request.add_header(key, headers[key]) 243 | response = urllib_request.urlopen(request) 244 | return HttpResponse(response) 245 | 246 | def _fetch(self, url, form_data={}, headers={}, compression=True): 247 | ''' 248 | Perform an HTTP GET or POST request. 249 | 250 | Args: 251 | url (str): The URL to GET or POST. 252 | 253 | form_data (dict): A dictionary of form data to POST. If empty, the 254 | request will be a GET, if it contains form data it will be a POST. 255 | 256 | Kwargs: 257 | headers (dict): A dictionary describing any headers you would like 258 | to add to the request. (eg. ``{'X-Test': 'testing'}``) 259 | 260 | compression (bool): If ``True`` (default), try to use gzip 261 | compression. 262 | 263 | Returns: 264 | An :class:`HttpResponse` object containing headers and other 265 | meta-information about the page and the page content. 266 | ''' 267 | req = urllib_request.Request(url) 268 | if form_data: 269 | if isinstance(form_data, string_types): 270 | form_data = form_data 271 | else: 272 | form_data = urlencode(form_data, True) 273 | req = urllib_request.Request(url, form_data) 274 | req.add_header('User-Agent', self._user_agent) 275 | for key in headers: 276 | req.add_header(key, headers[key]) 277 | if compression: 278 | req.add_header('Accept-Encoding', 'gzip') 279 | req.add_unredirected_header('Host', req.get_host()) 280 | response = urllib_request.urlopen(req) 281 | return HttpResponse(response) 282 | 283 | 284 | class HttpResponse: 285 | ''' 286 | This class represents a resoponse from an HTTP request. 287 | 288 | The content is examined and every attempt is made to properly encode it to 289 | Unicode. 290 | 291 | .. seealso:: 292 | :meth:`Net.http_GET`, :meth:`Net.http_HEAD` and :meth:`Net.http_POST` 293 | ''' 294 | 295 | content = '' 296 | '''Unicode encoded string containing the body of the reposne.''' 297 | 298 | def __init__(self, response): 299 | ''' 300 | Args: 301 | response (:class:`mimetools.Message`): The object returned by a call 302 | to :func:`urllib2.urlopen`. 303 | ''' 304 | self._response = response 305 | 306 | @property 307 | def content(self): 308 | html = self._response.read() 309 | encoding = None 310 | try: 311 | if self._response.headers['content-encoding'].lower() == 'gzip': 312 | html = gzip.GzipFile(fileobj=StringIO(html)).read() 313 | except: 314 | pass 315 | 316 | try: 317 | content_type = self._response.headers['content-type'] 318 | if 'charset=' in content_type: 319 | encoding = content_type.split('charset=')[-1] 320 | except: 321 | pass 322 | 323 | r = re.search('|%s|' % (mode,str(f)), xbmc.LOGDEBUG) 47 | self.func_registry[mode.strip()] = f 48 | self.args_registry[mode] = args 49 | self.kwargs_registry[mode] = kwargs 50 | # log_utils.log('registering args: |%s|-->(%s) and {%s}' % (mode, args, kwargs), xbmc.LOGDEBUG) 51 | 52 | return f 53 | 54 | return decorator 55 | 56 | def dispatch(self, mode, queries): 57 | """ 58 | Dispatch function to execute function registered for the provided mode 59 | 60 | mode: the string that the function was associated with 61 | queries: a dictionary of the parameters to be passed to the called function 62 | """ 63 | if mode not in self.func_registry: 64 | message = 'Error: Attempt to invoke unregistered mode |%s|' % (mode) 65 | log_utils.log(message, log_utils.LOGERROR) 66 | raise Exception(message) 67 | 68 | args = [] 69 | kwargs = {} 70 | unused_args = queries.copy() 71 | if self.args_registry[mode]: 72 | # positional arguments are all required 73 | for arg in self.args_registry[mode]: 74 | arg = arg.strip() 75 | if arg in queries: 76 | args.append(self.__coerce(queries[arg])) 77 | del unused_args[arg] 78 | else: 79 | message = 'Error: mode |%s| requested argument |%s| but it was not provided.' % (mode, arg) 80 | log_utils.log(message, log_utils.LOGERROR) 81 | raise Exception(message) 82 | 83 | if self.kwargs_registry[mode]: 84 | # kwargs are optional 85 | for arg in self.kwargs_registry[mode]: 86 | arg = arg.strip() 87 | if arg in queries: 88 | kwargs[arg] = self.__coerce(queries[arg]) 89 | del unused_args[arg] 90 | 91 | if 'mode' in unused_args: del unused_args['mode'] # delete mode last in case it's used by the target function 92 | log_utils.log('Calling |%s| for mode |%s| with pos args |%s| and kwargs |%s|' % ( 93 | self.func_registry[mode].__name__, mode, args, kwargs)) 94 | if unused_args: log_utils.log('Warning: Arguments |%s| were passed but unused by |%s| for mode |%s|' % 95 | (unused_args, self.func_registry[mode].__name__, mode)) 96 | self.func_registry[mode](*args, **kwargs) 97 | 98 | # since all params are passed as strings, do any conversions necessary to get good types (e.g. boolean) 99 | @staticmethod 100 | def __coerce(arg): 101 | temp = arg.lower() 102 | if temp == 'true': 103 | return True 104 | elif temp == 'false': 105 | return False 106 | elif temp == 'none': 107 | return None 108 | 109 | return arg 110 | -------------------------------------------------------------------------------- /resources/lib/addon_lib/urlresolver_helpers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2016 t0mm0, tknorris (URLResolver Addon for Kodi) 5 | Copyright (C) 2016-2019 anxdpanic 6 | 7 | This file is part of PlayThis (plugin.video.playthis) 8 | 9 | SPDX-License-Identifier: GPL-3.0-only 10 | See LICENSES/GPL-3.0-only for more information. 11 | """ 12 | 13 | import re 14 | 15 | from six.moves.urllib_parse import quote_plus 16 | 17 | from . import jsunpack 18 | from . import log_utils 19 | from .kodi import i18n 20 | 21 | 22 | class ResolverError(Exception): 23 | pass 24 | 25 | 26 | def get_hidden(html, form_id=None, index=None, include_submit=True): 27 | hidden = {} 28 | if form_id: 29 | pattern = '''
]*(?:id|name)\s*=\s*['"]?%s['"]?[^>]*>(.*?)
''' % (form_id) 30 | else: 31 | pattern = ''']*>(.*?)''' 32 | 33 | html = cleanse_html(html) 34 | 35 | for i, form in enumerate(re.finditer(pattern, html, re.DOTALL | re.I)): 36 | log_utils.log(form.group(1)) 37 | if index is None or i == index: 38 | for field in re.finditer(''']*type=['"]?hidden['"]?[^>]*>''', form.group(1)): 39 | match = re.search('''name\s*=\s*['"]([^'"]+)''', field.group(0)) 40 | match1 = re.search('''value\s*=\s*['"]([^'"]*)''', field.group(0)) 41 | if match and match1: 42 | hidden[match.group(1)] = match1.group(1) 43 | 44 | if include_submit: 45 | match = re.search(''']*type=['"]?submit['"]?[^>]*>''', form.group(1)) 46 | if match: 47 | name = re.search('''name\s*=\s*['"]([^'"]+)''', match.group(0)) 48 | value = re.search('''value\s*=\s*['"]([^'"]*)''', match.group(0)) 49 | if name and value: 50 | hidden[name.group(1)] = value.group(1) 51 | 52 | log_utils.log('Hidden fields are: %s' % (hidden)) 53 | return hidden 54 | 55 | 56 | def pick_source(sources): 57 | if len(sources) >= 1: 58 | return sources[0][1] 59 | else: 60 | raise ResolverError(i18n('no_video_link')) 61 | 62 | 63 | def append_headers(headers): 64 | return '|%s' % '&'.join(['%s=%s' % (key, quote_plus(headers[key])) for key in headers]) 65 | 66 | 67 | def get_packed_data(html): 68 | packed_data = '' 69 | for match in re.finditer('(eval\s*\(function.*?)', html, re.DOTALL | re.I): 70 | try: 71 | js_data = jsunpack.unpack(match.group(1)) 72 | js_data = js_data.replace('\\', '') 73 | packed_data += js_data 74 | except: 75 | pass 76 | 77 | return packed_data 78 | 79 | 80 | def parse_sources_list(html): 81 | sources = [] 82 | match = re.search('''['"]?sources['"]?\s*:\s*\[(.*?)\]''', html, re.DOTALL) 83 | if match: 84 | sources = [(match[1], match[0].replace('\/', '/')) for match in re.findall('''['"]?file['"]?\s*:\s*['"]([^'"]+)['"][^}]*['"]?label['"]?\s*:\s*['"]([^'"]*)''', match.group(1), re.DOTALL)] 85 | return sources 86 | 87 | 88 | def parse_html5_source_list(html): 89 | label_attrib = 'type' if not re.search('''''', html) else 'data-res' 90 | sources = [(match[1], match[0].replace('\/', '/')) for match in re.findall(''' 1) and (i.group(2) is not None): 100 | label = i.group(2) 101 | sources += [(label, '%s playpath=%s' % (base, i.group(1)))] 102 | return sources 103 | 104 | 105 | def cleanse_html(html): 106 | for match in re.finditer('', html, re.DOTALL): 107 | if match.group(1)[-2:] != '//': html = html.replace(match.group(0), '') 108 | 109 | html = re.sub('''<(div|span)[^>]+style=["'](visibility:\s*hidden|display:\s*none);?["']>.*?''', '', html, re.I | re.DOTALL) 110 | return html 111 | -------------------------------------------------------------------------------- /resources/lib/addon_lib/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 4 | Copyright (C) 2016-2019 anxdpanic 5 | 6 | This file is part of PlayThis (plugin.video.playthis) 7 | 8 | SPDX-License-Identifier: GPL-3.0-only 9 | See LICENSES/GPL-3.0-only for more information. 10 | """ 11 | 12 | import re 13 | import time 14 | 15 | from six import PY2 16 | from six.moves.urllib_parse import quote 17 | from six.moves.urllib_parse import unquote 18 | 19 | from . import kodi 20 | from . import log_utils 21 | from .constants import DATABASE 22 | from .constants import MODES 23 | from .remote import HttpJSONRPC 24 | 25 | 26 | class PlayHistory: 27 | OLD_TABLE = 'play_0_0_1' 28 | TABLE = 'play_0_0_2' 29 | ID = kodi.get_id() 30 | 31 | def __init__(self): 32 | self.create_table() 33 | 34 | @staticmethod 35 | def size_limit(): 36 | return int(kodi.get_setting('history-size-limit')) 37 | 38 | @staticmethod 39 | def use_directory(): 40 | if kodi.get_setting('history-list-type') == '1': 41 | return True 42 | else: 43 | return False 44 | 45 | def vacuum(self, table=None): 46 | if table is None: 47 | table = self.TABLE 48 | DATABASE.execute('VACUUM {0!s}'.format(table)) 49 | 50 | def add(self, url, content_type, label=None, thumb=''): 51 | if label is None: 52 | label = url 53 | label = unquote(label) 54 | thumb = unquote(thumb) 55 | execute = 'INSERT INTO {0!s} (addon_id, url, content_type, label, thumbnail) VALUES (?, ?, ?, ?, ?)'.format(self.TABLE) 56 | inserted = DATABASE.execute(execute, (self.ID, str(url), str(content_type), label, thumb)) 57 | if inserted == 1: 58 | execute = 'SELECT COUNT(*) FROM {0!s} WHERE addon_id=?'.format(self.TABLE) 59 | result = int(DATABASE.fetch(execute, (self.ID,))[0][0]) 60 | if result > self.size_limit(): 61 | execute = 'DELETE FROM {0!s} WHERE ROWID = (SELECT MIN(ROWID) FROM {0!s}) AND addon_id=?'.format(self.TABLE) 62 | result, rowcount = DATABASE.execute_w_rowcount(execute, (self.ID,)) 63 | if rowcount < 1: 64 | execute = 'DELETE * FROM {0!s} WHERE addon_id=?'.format(self.TABLE) 65 | result, rowcount = DATABASE.execute_w_rowcount(execute, (self.ID,)) 66 | if rowcount < 1: 67 | result = DATABASE.execute('DROP TABLE {0!s}'.format(self.TABLE)) 68 | self.vacuum() 69 | self.create_table() 70 | if rowcount > 0: 71 | self.vacuum() 72 | 73 | def delete_url(self, url): 74 | execute = 'DELETE FROM {0!s} WHERE url=? AND addon_id=?'.format(self.TABLE) 75 | result, rowcount = DATABASE.execute_w_rowcount(execute, (url, self.ID)) 76 | if result != 1: 77 | kodi.notify(msg=kodi.i18n('delete_failed'), sound=False) 78 | if rowcount > 0: 79 | self.vacuum() 80 | return result, rowcount 81 | 82 | def delete_row_id(self, row_id): 83 | execute = 'DELETE FROM {0!s} WHERE id=? AND addon_id=?'.format(self.TABLE) 84 | result, rowcount = DATABASE.execute_w_rowcount(execute, (row_id, self.ID)) 85 | if result != 1: 86 | kodi.notify(msg=kodi.i18n('delete_failed'), sound=False) 87 | return result, rowcount 88 | 89 | def rename_row_id(self, row_id, label): 90 | execute = 'UPDATE {0!s} SET label=? WHERE id=? AND addon_id=?'.format(self.TABLE) 91 | result = DATABASE.execute(execute, (label, row_id, self.ID)) 92 | if result != 1: 93 | kodi.notify(msg=kodi.i18n('rename_failed'), sound=False) 94 | return result 95 | 96 | def change_thumb(self, row_id, thumb): 97 | execute = 'UPDATE {0!s} SET thumbnail=? WHERE id=? AND addon_id=?'.format(self.TABLE) 98 | result = DATABASE.execute(execute, (unquote(thumb), row_id, self.ID)) 99 | if result != 1: 100 | kodi.notify(msg=kodi.i18n('thumbchange_failed'), sound=False) 101 | return result 102 | 103 | def get(self, include_ids=False, row_id=None): 104 | if row_id is None: 105 | execute = 'SELECT * FROM {0!s} WHERE addon_id=? ORDER BY id DESC'.format(self.TABLE) 106 | selected = DATABASE.fetch(execute, (self.ID,)) 107 | else: 108 | execute = 'SELECT * FROM {0!s} WHERE id=? AND addon_id=?'.format(self.TABLE) 109 | selected = DATABASE.fetch(execute, (row_id, self.ID)) 110 | results = [] 111 | if selected: 112 | for id_key, addon_id, query, content_type, label, thumbnail in selected: 113 | if not include_ids: 114 | results.extend([(unquote(query), content_type, label, unquote(thumbnail))]) 115 | else: 116 | results.extend([(id_key, unquote(query), content_type, label, unquote(thumbnail))]) 117 | return results 118 | else: 119 | return [] 120 | 121 | def clear(self, ctype=None): 122 | if ctype is None: 123 | result = DATABASE.execute('DROP TABLE {0!s}'.format(self.TABLE), '') 124 | else: 125 | result = DATABASE.execute('DELETE FROM {0!s} WHERE content_type=?'.format(self.TABLE), (ctype,)) 126 | if result == 1: 127 | self.vacuum() 128 | kodi.notify(msg=kodi.i18n('history_cleared'), sound=False) 129 | else: 130 | kodi.notify(msg=kodi.i18n('fail_history_clear'), sound=False) 131 | 132 | def get_input(self): 133 | got_input = kodi.get_keyboard(kodi.i18n('enter_for_playback'), '') 134 | if got_input: 135 | got_input = got_input.strip() 136 | got_input = quote(re.sub(r'\s+', ' ', got_input)) 137 | return got_input 138 | return '' 139 | 140 | def history_dialog(self, ctype): 141 | if self.size_limit() != 0: 142 | _queries = self.get() 143 | if len(_queries) > 0: 144 | queries = [] 145 | for item, content_type, label in _queries: 146 | if content_type == ctype: 147 | queries += [label] 148 | if len(queries) > 0: 149 | queries.insert(0, '[B]{0!s}[/B]'.format(kodi.i18n('new_'))) 150 | queries.insert(1, '[B]{0!s}[/B]'.format(kodi.i18n('clear_history'))) 151 | index = kodi.Dialog().select(kodi.i18n('choose_playback'), queries) 152 | if index > -1: 153 | if index == 1: 154 | self.clear() 155 | return '' 156 | elif index > 1: 157 | return queries[index] 158 | else: 159 | return '' 160 | return self.get_input() 161 | 162 | def history_directory(self, ctype): 163 | icon_path = kodi.get_icon() 164 | fanart_path = kodi.get_fanart() 165 | total_items = None 166 | if self.size_limit() != 0: 167 | _queries = self.get(include_ids=True) 168 | queries = [] 169 | for index, (row_id, item, content_type, label, thumbnail) in enumerate(_queries): 170 | if content_type == ctype: 171 | queries += [_queries[index]] 172 | if len(queries) > 0: 173 | total_items = len(queries) 174 | 175 | can_remote_send = HttpJSONRPC().has_connection_details 176 | resolve_locally = kodi.get_setting('resolve-locally') == 'true' 177 | 178 | for row_id, item, content_type, label, thumbnail in queries: 179 | play_path = {'mode': MODES.PLAY, 'player': 'false', 'history': 'false', 'path': quote(item), 'thumb': quote(thumbnail)} 180 | if ctype == 'image': 181 | play_path = item 182 | menu_items = [(kodi.i18n('new_'), 'RunPlugin(%s)' % 183 | (kodi.get_plugin_url({'mode': MODES.NEW, 'player': 'true'}))), 184 | (kodi.i18n('manage'), 'Container.Update(%s)' % 185 | (kodi.get_plugin_url({'mode': MODES.MANAGE_MENU, 'row_id': row_id, 'title': quote(label)}))), 186 | (kodi.i18n('export'), 'Container.Update(%s)' % 187 | (kodi.get_plugin_url({'mode': MODES.EXPORT_MENU, 'row_id': row_id, 'ctype': content_type}))), 188 | (kodi.i18n('clear_history'), 'RunPlugin(%s)' % 189 | (kodi.get_plugin_url({'mode': MODES.CLEARHISTORY, 'ctype': content_type}))), 190 | (kodi.i18n('refresh'), 'Container.Refresh')] 191 | 192 | if can_remote_send: 193 | if resolve_locally: 194 | send_path = {'mode': MODES.PLAY, 'path': quote(item), 'thumb': quote(thumbnail), 'title': quote(label), 'player': 'remote'} 195 | else: 196 | send_path = {'mode': MODES.SENDREMOTE, 'path': quote(item), 'thumb': quote(thumbnail), 'title': quote(label)} 197 | menu_items.append((kodi.i18n('send_remote_playthis'), 'RunPlugin(%s)' % (kodi.get_plugin_url(send_path)))) 198 | 199 | is_folder = False 200 | thumb = icon_path 201 | if content_type == 'image': 202 | thumb = item 203 | if thumbnail: 204 | thumb = thumbnail 205 | info = {'title': label} 206 | if content_type == 'audio': 207 | info.update({'mediatype': 'song'}) 208 | elif content_type == 'video': 209 | info.update({'mediatype': 'video'}) 210 | elif content_type == 'executable': 211 | is_folder = True 212 | play_path['player'] = 'true' 213 | 214 | log_utils.log('Creating item |{2}|: path |{0}| content type |{1}|'.format(play_path, content_type, label), log_utils.LOGDEBUG) 215 | kodi.create_item(play_path, 216 | label, thumb=thumb, fanart=fanart_path, is_folder=is_folder, 217 | is_playable=True, total_items=total_items, menu_items=menu_items, 218 | content_type=content_type, info=info) 219 | if not total_items: 220 | menu_items = [(kodi.i18n('refresh'), 'Container.Refresh')] 221 | kodi.create_item({'mode': MODES.NEW, 'player': 'true'}, kodi.i18n('new_'), thumb=icon_path, 222 | fanart=fanart_path, is_folder=False, is_playable=False, menu_items=menu_items) 223 | kodi.end_of_directory(cache_to_disc=False) 224 | 225 | def create_table(self): 226 | DATABASE.execute('CREATE TABLE IF NOT EXISTS {0!s} (id INTEGER PRIMARY KEY AUTOINCREMENT, ' 227 | 'addon_id, url, content_type TEXT DEFAULT "video", label TEXT DEFAULT "Unknown", ' 228 | 'thumbnail TEXT DEFAULT "", CONSTRAINT unq UNIQUE (addon_id, url, content_type) )'.format(self.TABLE), '') 229 | DATABASE.execute('''CREATE TRIGGER IF NOT EXISTS default_label_url 230 | AFTER INSERT ON {0!s} 231 | WHEN new.label="Unknown" 232 | BEGIN 233 | UPDATE {0!s} SET label=new.url WHERE id=new.id; 234 | END 235 | ; 236 | '''.format(self.TABLE), '') 237 | DATABASE.execute('ALTER TABLE {0!s} ADD COLUMN thumbnail TEXT DEFAULT ""'.format(self.TABLE), '', suppress=True) 238 | 239 | exists = DATABASE.fetch('SELECT name FROM sqlite_master WHERE type="table" AND name=?', (self.OLD_TABLE,)) 240 | if exists: 241 | DATABASE.execute('INSERT INTO {0!s} (addon_id, url) SELECT addon_id, url FROM {1!s}'.format(self.TABLE, self.OLD_TABLE), '') 242 | DATABASE.execute('ALTER TABLE {0!s} RENAME TO {1!s}'.format(self.OLD_TABLE, '{0!s}_bak'.format(self.OLD_TABLE)), '') 243 | 244 | 245 | class M3UUtils: 246 | def __init__(self, filename, from_list='history'): 247 | if not from_list: 248 | from_list = 'history' 249 | self.from_list = from_list 250 | self.filename = filename if filename.endswith('.m3u') else filename + '.m3u' 251 | 252 | def _get(self): 253 | log_utils.log('M3UUtils._get from_list: |{0!s}|'.format(self.from_list), log_utils.LOGDEBUG) 254 | if self.from_list == 'history': 255 | return PlayHistory().get() 256 | else: 257 | return [] 258 | 259 | def export(self, results='playthis', ctype='video'): 260 | if results == 'resolved': 261 | from .playback import resolve 262 | else: 263 | def resolve(url): 264 | return url 265 | rows = self._get() 266 | if rows: 267 | _m3u = '#EXTM3U\n' 268 | m3u = _m3u 269 | for item, content_type, title, thumb in rows: 270 | if content_type != ctype: 271 | continue 272 | if results == 'resolved': 273 | resolved = resolve(item) 274 | else: 275 | resolved = None 276 | if resolved: 277 | log_utils.log('M3UUtils.export adding resolved item: |{0!s}| as |{1!s}|'.format(resolved, title), 278 | log_utils.LOGDEBUG) 279 | m3u += '#EXTINF:{0!s} tvg-logo="{3!s}",{1!s}\n{2!s}\n'.format('0', title, resolved, thumb) 280 | else: 281 | if results == 'playthis': 282 | pt_url = 'plugin://plugin.video.playthis/?mode=play&player=false&history=false&path={0!s}' \ 283 | .format(quote(item)) 284 | log_utils.log('M3UUtils.export adding PlayThis item: |{0!s}| as |{1!s}|'.format(pt_url, title), 285 | log_utils.LOGDEBUG) 286 | m3u += '#EXTINF:{0!s} tvg-logo="{3!s}",{1!s}\n{2!s}\n'.format('0', title, pt_url, thumb) 287 | else: 288 | log_utils.log('M3UUtils.export adding unresolved item: |{0!s}| as |{1!s}|'.format(item, title), 289 | log_utils.LOGDEBUG) 290 | m3u += '#EXTINF:{0!s} tvg-logo="{3!s}",{1!s}\n{2!s}\n'.format('0', title, item, thumb) 291 | 292 | if m3u != _m3u: 293 | log_utils.log('M3UUtils.export writing .m3u: |{0!s}|'.format(self.filename), log_utils.LOGDEBUG) 294 | if not PY2: 295 | m3u = bytes(m3u, encoding='utf-8') 296 | try: 297 | with open(self.filename, 'wb+') as f: 298 | f.write(m3u) 299 | log_utils.log('M3UUtils.export writing .m3u completed.', log_utils.LOGDEBUG) 300 | kodi.notify(msg=kodi.i18n('export_success'), sound=False) 301 | return 302 | except: 303 | log_utils.log('M3UUtils.export failed to write .m3u', log_utils.LOGDEBUG) 304 | kodi.notify(msg=kodi.i18n('export_fail'), sound=False) 305 | return 306 | log_utils.log('M3UUtils.export no items for export to .m3u', log_utils.LOGDEBUG) 307 | kodi.notify(msg=kodi.i18n('no_items_export'), sound=False) 308 | 309 | 310 | class STRMUtils: 311 | def __init__(self, filename): 312 | self.filename = filename if filename.endswith('.strm') else filename + '.strm' 313 | 314 | def _get(self, row_id): 315 | return PlayHistory().get(row_id=row_id) 316 | 317 | def export(self, row_id): 318 | rows = self._get(row_id) 319 | if rows: 320 | url, content_type, title, thumb = rows[0] 321 | play_path = {'mode': MODES.PLAY, 'player': 'false', 'history': 'false', 'path': quote(url), 'thumb': quote(thumb)} 322 | strm = kodi.get_plugin_url(play_path) 323 | 324 | if strm: 325 | log_utils.log('STRMUtils.export writing .m3u: |{0!s}|'.format(self.filename), log_utils.LOGDEBUG) 326 | if not PY2: 327 | strm = bytes(strm, encoding='utf-8') 328 | try: 329 | with open(self.filename, 'wb+') as f: 330 | f.write(strm) 331 | log_utils.log('STRMUtils.export writing .m3u completed.', log_utils.LOGDEBUG) 332 | kodi.notify(msg=kodi.i18n('export_success'), sound=False) 333 | return 334 | except: 335 | log_utils.log('STRMUtils.export failed to write .strm', log_utils.LOGDEBUG) 336 | kodi.notify(msg=kodi.i18n('export_fail'), sound=False) 337 | return 338 | log_utils.log('STRMUtils.export no item for export to .strm', log_utils.LOGDEBUG) 339 | kodi.notify(msg=kodi.i18n('no_items_export'), sound=False) 340 | 341 | 342 | def wait_for_busy_dialog(): 343 | """ 344 | Wait for busy dialogs to close, starting playback while the busy dialog is active 345 | could crash Kodi 18 / 19 (pre-alpha) 346 | 347 | Github issues: 348 | https://github.com/xbmc/xbmc/issues/16756 349 | https://github.com/xbmc/xbmc/pull/16450 # possible solution 350 | 351 | TODO: remove this function when the above issue is resolved 352 | """ 353 | monitor = kodi.Monitor() 354 | start_time = time.time() 355 | kodi.sleep(500) 356 | 357 | def _abort(): 358 | return monitor.abortRequested() 359 | 360 | def _busy(): 361 | return kodi.getCurrentWindowDialogId() in [10138, 10160] 362 | 363 | def _wait(): 364 | log_utils.log('Waiting for busy dialogs to close ...', log_utils.LOGDEBUG) 365 | while not _abort() and _busy(): 366 | if monitor.waitForAbort(1): 367 | break 368 | 369 | while not _abort(): 370 | if _busy(): 371 | _wait() 372 | 373 | if monitor.waitForAbort(1): 374 | break 375 | 376 | if not _busy(): 377 | break 378 | 379 | log_utils.log('Waited %.2f for busy dialogs to close.' % 380 | (time.time() - start_time), log_utils.LOGDEBUG) 381 | return not _abort() and not _busy() 382 | -------------------------------------------------------------------------------- /resources/media/screenshots/screenshot000.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anxdpanic/plugin.video.playthis/8ce16be445f784a6dd6c60a8bc2ea0b95765d728/resources/media/screenshots/screenshot000.jpg -------------------------------------------------------------------------------- /resources/media/screenshots/screenshot001.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anxdpanic/plugin.video.playthis/8ce16be445f784a6dd6c60a8bc2ea0b95765d728/resources/media/screenshots/screenshot001.jpg -------------------------------------------------------------------------------- /resources/media/screenshots/screenshot002.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anxdpanic/plugin.video.playthis/8ce16be445f784a6dd6c60a8bc2ea0b95765d728/resources/media/screenshots/screenshot002.jpg -------------------------------------------------------------------------------- /resources/media/screenshots/screenshot003.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anxdpanic/plugin.video.playthis/8ce16be445f784a6dd6c60a8bc2ea0b95765d728/resources/media/screenshots/screenshot003.jpg -------------------------------------------------------------------------------- /resources/media/screenshots/screenshot004.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anxdpanic/plugin.video.playthis/8ce16be445f784a6dd6c60a8bc2ea0b95765d728/resources/media/screenshots/screenshot004.jpg -------------------------------------------------------------------------------- /resources/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | --------------------------------------------------------------------------------