├── .gitignore ├── CONTRIBUTING.rst ├── LICENSE ├── README.rst ├── app.py ├── bin └── capture.sh ├── media └── .gitignore ├── requirements.txt ├── requirements_dev.txt ├── samsungtv ├── __init__.py ├── __main__.py ├── app.py ├── dlna │ ├── __init__.py │ ├── device.py │ ├── device_services.py │ ├── devices.py │ └── utils.py ├── httpd │ ├── __init__.py │ ├── composite_handler.py │ ├── proxy_handler.py │ ├── server.py │ ├── serverctrl.py │ └── subscribe_handler.py ├── services │ ├── __init__.py │ ├── dial.py │ ├── remote_control.py │ └── ssdp.py ├── upnpevents │ ├── __init__.py │ └── subscriber.py └── upnpservice │ ├── __init__.py │ ├── avtransport.py │ ├── base.py │ ├── connectionmanager.py │ ├── dialreceiver.py │ ├── rendering.py │ └── wfaconfig.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── mocks.py ├── test_dial_service.py ├── test_dlna_device.py ├── test_dlna_devices.py ├── test_event_subscriber.py ├── test_proxy_http_handler.py ├── test_remote_control.py ├── test_ssdp_discovery.py ├── test_subscribe_http_handler.py ├── test_upnpservice_av_transport.py ├── test_upnpservice_connection_manager.py ├── test_upnpservice_rendering.py └── test_upnpservice_wfa_config.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ./cache/* 3 | ./media/* 4 | 5 | # Created by https://www.gitignore.io/api/python,eclipse,phpstorm 6 | 7 | ### Eclipse ### 8 | 9 | .metadata 10 | bin/ 11 | tmp/ 12 | *.tmp 13 | *.bak 14 | *.swp 15 | *~.nib 16 | local.properties 17 | .settings/ 18 | .loadpath 19 | .recommenders 20 | 21 | # External tool builders 22 | .externalToolBuilders/ 23 | 24 | # Locally stored "Eclipse launch configurations" 25 | *.launch 26 | 27 | # PyDev specific (Python IDE for Eclipse) 28 | *.pydevproject 29 | 30 | # CDT-specific (C/C++ Development Tooling) 31 | .cproject 32 | 33 | # Java annotation processor (APT) 34 | .factorypath 35 | 36 | # PDT-specific (PHP Development Tools) 37 | .buildpath 38 | 39 | # sbteclipse plugin 40 | .target 41 | 42 | # Tern plugin 43 | .tern-project 44 | 45 | # TeXlipse plugin 46 | .texlipse 47 | 48 | # STS (Spring Tool Suite) 49 | .springBeans 50 | 51 | # Code Recommenders 52 | .recommenders/ 53 | 54 | # Scala IDE specific (Scala & Java development for Eclipse) 55 | .cache-main 56 | .scala_dependencies 57 | .worksheet 58 | 59 | ### Eclipse Patch ### 60 | # Eclipse Core 61 | .project 62 | 63 | # JDT-specific (Eclipse Java Development Tools) 64 | .classpath 65 | 66 | ### PhpStorm ### 67 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 68 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 69 | 70 | # User-specific stuff: 71 | .idea/**/workspace.xml 72 | .idea/**/tasks.xml 73 | .idea/dictionaries 74 | 75 | # Sensitive or high-churn files: 76 | .idea/**/dataSources/ 77 | .idea/**/dataSources.ids 78 | .idea/**/dataSources.xml 79 | .idea/**/dataSources.local.xml 80 | .idea/**/sqlDataSources.xml 81 | .idea/**/dynamic.xml 82 | .idea/**/uiDesigner.xml 83 | 84 | # Gradle: 85 | .idea/**/gradle.xml 86 | .idea/**/libraries 87 | 88 | # CMake 89 | cmake-build-debug/ 90 | 91 | # Mongo Explorer plugin: 92 | .idea/**/mongoSettings.xml 93 | 94 | ## File-based project format: 95 | *.iws 96 | 97 | ## Plugin-specific files: 98 | 99 | # IntelliJ 100 | /out/ 101 | 102 | # mpeltonen/sbt-idea plugin 103 | .idea_modules/ 104 | 105 | # JIRA plugin 106 | atlassian-ide-plugin.xml 107 | 108 | # Cursive Clojure plugin 109 | .idea/replstate.xml 110 | 111 | # Ruby plugin and RubyMine 112 | /.rakeTasks 113 | 114 | # Crashlytics plugin (for Android Studio and IntelliJ) 115 | com_crashlytics_export_strings.xml 116 | crashlytics.properties 117 | crashlytics-build.properties 118 | fabric.properties 119 | 120 | ### PhpStorm Patch ### 121 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 122 | 123 | # *.iml 124 | # modules.xml 125 | # .idea/misc.xml 126 | # *.ipr 127 | 128 | # Sonarlint plugin 129 | .idea/sonarlint 130 | 131 | ### Python ### 132 | # Byte-compiled / optimized / DLL files 133 | __pycache__/ 134 | *.py[cod] 135 | *$py.class 136 | 137 | # C extensions 138 | *.so 139 | 140 | # Distribution / packaging 141 | .Python 142 | build/ 143 | develop-eggs/ 144 | dist/ 145 | downloads/ 146 | eggs/ 147 | .eggs/ 148 | lib/ 149 | lib64/ 150 | parts/ 151 | sdist/ 152 | var/ 153 | wheels/ 154 | *.egg-info/ 155 | .installed.cfg 156 | *.egg 157 | 158 | # PyInstaller 159 | # Usually these files are written by a python script from a template 160 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 161 | *.manifest 162 | *.spec 163 | 164 | # Installer logs 165 | pip-log.txt 166 | pip-delete-this-directory.txt 167 | 168 | # Unit test / coverage reports 169 | htmlcov/ 170 | .tox/ 171 | .coverage 172 | .coverage.* 173 | .cache 174 | .pytest_cache/ 175 | nosetests.xml 176 | coverage.xml 177 | *.cover 178 | .hypothesis/ 179 | 180 | # Translations 181 | *.mo 182 | *.pot 183 | 184 | # Flask stuff: 185 | instance/ 186 | .webassets-cache 187 | 188 | # Scrapy stuff: 189 | .scrapy 190 | 191 | # Sphinx documentation 192 | docs/_build/ 193 | 194 | # PyBuilder 195 | target/ 196 | 197 | # Jupyter Notebook 198 | .ipynb_checkpoints 199 | 200 | # pyenv 201 | .python-version 202 | 203 | # celery beat schedule file 204 | celerybeat-schedule.* 205 | 206 | # SageMath parsed files 207 | *.sage.py 208 | 209 | # Environments 210 | .env 211 | .venv 212 | env/ 213 | venv/ 214 | ENV/ 215 | env.bak/ 216 | venv.bak/ 217 | 218 | # Spyder project settings 219 | .spyderproject 220 | .spyproject 221 | 222 | # Rope project settings 223 | .ropeproject 224 | 225 | # mkdocs documentation 226 | /site 227 | 228 | # mypy 229 | .mypy_cache/ 230 | 231 | 232 | # End of https://www.gitignore.io/api/python,eclipse,phpstorm 233 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: shell 2 | 3 | ============ 4 | Contributing 5 | ============ 6 | 7 | Contributions are welcome, and they are greatly appreciated! Every little bit 8 | helps, and credit will always be given. 9 | 10 | You can contribute in many ways: 11 | 12 | Types of Contributions 13 | ---------------------- 14 | 15 | Report Bugs 16 | ~~~~~~~~~~~ 17 | 18 | Report bugs at https://github.com/svilborg/samsungtv/issues. 19 | 20 | If you are reporting a bug, please include: 21 | 22 | * Your operating system name and version. 23 | * Any details about your local setup that might be helpful in troubleshooting. 24 | * Detailed steps to reproduce the bug. 25 | 26 | Fix Bugs 27 | ~~~~~~~~ 28 | 29 | Look through the GitHub issues for bugs. Anything tagged with "bug" and "help 30 | wanted" is open to whoever wants to implement it. 31 | 32 | Implement Features 33 | ~~~~~~~~~~~~~~~~~~ 34 | 35 | Look through the GitHub issues for features. Anything tagged with "enhancement" 36 | and "help wanted" is open to whoever wants to implement it. 37 | 38 | Write Documentation 39 | ~~~~~~~~~~~~~~~~~~~ 40 | 41 | Samsung TV could always use more documentation, whether as part of the 42 | official Samsung TV docs, in docstrings, or even on the web in blog posts, 43 | articles, and such. 44 | 45 | Submit Feedback 46 | ~~~~~~~~~~~~~~~ 47 | 48 | The best way to send feedback is to file an issue at https://github.com/svilborg/samsungtv/issues. 49 | 50 | If you are proposing a feature: 51 | 52 | * Explain in detail how it would work. 53 | * Keep the scope as narrow as possible, to make it easier to implement. 54 | * Remember that this is a volunteer-driven project, and that contributions 55 | are welcome :) 56 | 57 | Get Started! 58 | ------------ 59 | 60 | Ready to contribute? Here's how to set up `samsungtv` for local development. 61 | 62 | 1. Fork the `samsungtv` repo on GitHub. 63 | 2. Clone your fork locally:: 64 | 65 | $ git clone git@github.com:your_name_here/samsungtv.git 66 | 67 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: 68 | 69 | $ mkvirtualenv samsungtv 70 | $ cd samsungtv/ 71 | $ python setup.py develop 72 | 73 | 4. Create a branch for local development:: 74 | 75 | $ git checkout -b name-of-your-bugfix-or-feature 76 | 77 | Now you can make your changes locally. 78 | 79 | 5. When you're done making changes, check that your changes pass flake8 and the 80 | tests, including testing other Python versions with tox:: 81 | 82 | $ flake8 samsungtv tests 83 | $ python setup.py test or py.test 84 | $ tox 85 | 86 | To get flake8 and tox, just pip install them into your virtualenv. 87 | 88 | 6. Commit your changes and push your branch to GitHub:: 89 | 90 | $ git add . 91 | $ git commit -m "Your detailed description of your changes." 92 | $ git push origin name-of-your-bugfix-or-feature 93 | 94 | 7. Submit a pull request through the GitHub website. 95 | 96 | Pull Request Guidelines 97 | ----------------------- 98 | 99 | Before you submit a pull request, check that it meets these guidelines: 100 | 101 | 1. The pull request should include tests. 102 | 2. If the pull request adds functionality, the docs should be updated. Put 103 | your new functionality into a function with a docstring, and add the 104 | feature to the list in README.rst. 105 | 3. The pull request should work for Python 2.7, 3.4, 3.5 and 3.6, and for PyPy. Check 106 | https://travis-ci.org/svilborg/samsungtv/pull_requests 107 | and make sure that the tests pass for all supported Python versions. 108 | 109 | Tips 110 | ---- 111 | 112 | To run a subset of tests:: 113 | 114 | 115 | $ python -m unittest tests.test_samsungtv 116 | 117 | Deploying 118 | --------- 119 | 120 | A reminder for the maintainers on how to deploy. 121 | Make sure all your changes are committed (including an entry in HISTORY.rst). 122 | Then run:: 123 | 124 | $ bumpversion patch # possible: major / minor / patch 125 | $ git push 126 | $ git push --tags 127 | 128 | Travis will then deploy to PyPI if tests pass. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Samsung TV 3 | ========== 4 | 5 | Samsung Tv UPnP/DIAL/Remote Automations 6 | 7 | 8 | * Free software: Apache license 2.0 9 | 10 | 11 | Features 12 | -------- 13 | 14 | 15 | Usage: samsungtv-cli [OPTIONS] 16 | 17 | --scan SSPD Scan 18 | --rescan SSPD Scan (refreshed cache) 19 | --volume Set Volume 20 | --volup Incr Volume 21 | --voldown Decr Volume 22 | --mute Mute 23 | --unmute Unmute 24 | --app_on Start App 25 | --app_off Stop App 26 | --file Play Media File 27 | --add_file Add Media File 28 | --play Play Media 29 | --stop Stop Media 30 | --key Send a Key 31 | --keys List Available Keys 32 | --launch Launch App 33 | --help This help menu 34 | --start_httpd Start http server 35 | --stop_httpd Stop http server 36 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import getopt 2 | import sys 3 | 4 | from samsungtv import SamsungTvApp 5 | 6 | 7 | def usage_option(name, description): 8 | return "\t--%s \t %s" % (name, description) 9 | 10 | 11 | def usage(): 12 | print "" 13 | print "Usage: " + sys.argv[0] + " [OPTIONS]" 14 | print usage_option("scan", "SSPD Scan") 15 | print usage_option("rescan", "SSPD Scan (refreshed cache)") 16 | print usage_option("volume", "Set Volume") 17 | print usage_option("volup", "Incr Volume") 18 | print usage_option("voldown", "Decr Volume") 19 | print usage_option("mute", "Mute") 20 | print usage_option("unmute", "Unmute") 21 | print usage_option("app_on", "Start App") 22 | print usage_option("app_off", "Stop App") 23 | print usage_option("file", "Play Media File") 24 | print usage_option("add_file", "Add Media File") 25 | print usage_option("play", "Play Media") 26 | print usage_option("stop", "Stop Media") 27 | print usage_option("key", "Send a Key") 28 | print usage_option("keys", "List Available Keys") 29 | print usage_option("launch", "Launch App") 30 | print usage_option("help", "This help menu") 31 | print usage_option("start_httpd", "Start http server") 32 | print usage_option("stop_httpd", "Stop http server") 33 | print "" 34 | 35 | 36 | if __name__ == "__main__": 37 | 38 | try: 39 | opts, args = getopt.getopt(sys.argv[1:], "s:hv", ["help", "version", 40 | "scan", "rescan", 41 | "volume=", "volup", "voldown", "mute", "unmute", 42 | "file=", "add_file=", 43 | "play", "stop", "next", "prev", 44 | "start_httpd", "stop_httpd", 45 | "app=", "app_on=", "app_off=", "app_install=", 46 | "key=", "keys", "apps", "launch=" 47 | ]) 48 | except getopt.GetoptError, err: 49 | print(err) 50 | sys.exit(-1) 51 | 52 | for o, arg in opts: 53 | if o in ("-h", "--help"): 54 | usage() 55 | sys.exit() 56 | elif o in ("-V", "--version"): 57 | print VERSION 58 | sys.exit(0) 59 | elif o in ("-s", "--scan"): 60 | SamsungTvApp.scan() 61 | elif o in ("--rescan"): 62 | SamsungTvApp.scan(True) 63 | else: 64 | tv = SamsungTvApp() 65 | 66 | method = o.replace("--", "") 67 | print tv.run(method, arg) 68 | -------------------------------------------------------------------------------- /bin/capture.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | pulseaudio-monitor() { 4 | echo $(pactl list | grep -A2 '^Source #' | grep 'Name: .*\.monitor$' | awk '{print $NF}' | tail -n1) 5 | } 6 | 7 | # cvlc screen:// \ 8 | # :sout=#transcode{vcodec=h264,vb=0,scale=0,acodec=mpga,ab=128,channels=2,samplerate=44100}:file{dst=./screen.mp4} :sout-keep 9 | 10 | # cvlc screen:// :screen-fps=25 :screen-caching=100 \ 11 | # --sout '#transcode{vcodec=MJPG,vb=0,width=1022,height=575,acodec=none}:http{mux=ogg,dst=:8554/myscreen}' 12 | 13 | # cvlc screen:// :screen-fps=25 :screen-caching=100 \ 14 | # --sout '#transcode{h264,vb=0,scale=0,acodec=mpga,ab=128,channels=2,samplerate=44100}:http{mux=ogg,dst=:8554/myscreen}' 15 | 16 | desktop-capture-ff() { 17 | 18 | local RESOLUTION="640x360" 19 | local QUAL="fast" 20 | local TUNE="animation" 21 | local OUTPUT="$(dirname "$0")/../media/capture.mov" 22 | local OFFSET="0,0" 23 | local FPS="30" 24 | 25 | if [[ -f "$OUTPUT" ]]; then 26 | rm "$OUTPUT" 27 | fi 28 | 29 | # avconv \ 30 | # -f alsa -i hw:0 \ 31 | # -f x11grab -r "$FPS" \ 32 | # -i :0.0+"$OFFSET" \ 33 | # -s "$RESOLUTION" \ 34 | # -vcodec libx264 -crf 38 \ 35 | # -preset "$QUAL" -tune "$TUNE" \ 36 | # -threads 2 \ 37 | # -an \ 38 | # "$OUTPUT" 39 | 40 | # -profile:v baseline -level 30 \ 41 | # -acodec copy \ 42 | # -acodec libmp3lame -ar 44100 \ 43 | 44 | ffmpeg -y -f alsa \ 45 | -i hw:0 \ 46 | -f x11grab -framerate 30 \ 47 | -i :0.0+0,0 \ 48 | -video_size 1024×576 \ 49 | -acodec ac3 -ac 1 \ 50 | -vcodec libx264 \ 51 | -preset fast \ 52 | -threads 0 \ 53 | -f matroska \ 54 | "$OUTPUT" 55 | 56 | } 57 | 58 | echo $(dirname "$0") 59 | desktop-capture-ff 60 | -------------------------------------------------------------------------------- /media/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svilborg/samsungtv/007d8cbf4326ae13d74ab138eb77798d11aae108/media/.gitignore -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | setuptools==38.5.1 2 | requests>=2.20.0 3 | websocket_client==0.47.0 -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | tox==2.9.1 2 | Sphinx==1.7.0 3 | coverage==4.5.1 4 | nose==1.3.7 5 | nose-cov==1.6 6 | nose-cover3==0.1.0 7 | mock==2.0.0 -------------------------------------------------------------------------------- /samsungtv/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Top-level package for Samsung TV.""" 4 | 5 | __author__ = """S.Simeonov""" 6 | __email__ = 'svilborg@gmail.com' 7 | __version__ = '0.1.0' 8 | 9 | 10 | from app import SamsungTvApp 11 | -------------------------------------------------------------------------------- /samsungtv/__main__.py: -------------------------------------------------------------------------------- 1 | import getopt 2 | import sys 3 | 4 | from samsungtv import SamsungTvApp 5 | 6 | def usage_option(name, description): 7 | return "\t--%s \t %s" % (name, description) 8 | 9 | 10 | def usage(): 11 | print "" 12 | print "Usage: " + sys.argv[0] + " [OPTIONS]" 13 | print usage_option("scan", "SSPD Scan") 14 | print usage_option("rescan", "SSPD Scan (refreshed cache)") 15 | print usage_option("volume", "Set Volume") 16 | print usage_option("volup", "Incr Volume") 17 | print usage_option("voldown", "Decr Volume") 18 | print usage_option("mute", "Mute") 19 | print usage_option("unmute", "Unmute") 20 | print usage_option("app_on", "Start App") 21 | print usage_option("app_off", "Stop App") 22 | print usage_option("file", "Play Media File") 23 | print usage_option("add_file", "Add Media File") 24 | print usage_option("play", "Play Media") 25 | print usage_option("stop", "Stop Media") 26 | print usage_option("key", "Send a Key") 27 | print usage_option("keys", "List Available Keys") 28 | print usage_option("launch", "Launch App") 29 | print usage_option("help", "This help menu") 30 | print usage_option("start_httpd", "Start http server") 31 | print usage_option("stop_httpd", "Stop http server") 32 | print "" 33 | 34 | def main(): 35 | try: 36 | opts, args = getopt.getopt(sys.argv[1:], "s:hv", ["help", "version", 37 | "scan", "rescan", 38 | "volume=", "volup", "voldown", "mute", "unmute", 39 | "file=", "add_file=", 40 | "play", "stop", "next", "prev", 41 | "start_httpd", "stop_httpd", 42 | "app=", "app_on=", "app_off=", "app_install=", 43 | "key=", "keys", "apps", "launch=" 44 | ]) 45 | except getopt.GetoptError, err: 46 | print(err) 47 | sys.exit(-1) 48 | 49 | for o, arg in opts: 50 | if o in ("-h", "--help"): 51 | usage() 52 | sys.exit() 53 | elif o in ("-V", "--version"): 54 | # print VERSION 55 | sys.exit(0) 56 | elif o in ("-s", "--scan"): 57 | SamsungTvApp.scan() 58 | elif o in ("--rescan"): 59 | SamsungTvApp.scan(True) 60 | else: 61 | tv = SamsungTvApp() 62 | 63 | method = o.replace("--", "") 64 | print tv.run(method, arg) 65 | 66 | 67 | if __name__ == "__main__": 68 | main() -------------------------------------------------------------------------------- /samsungtv/app.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from httpd import HttpProxyServerCtrl 4 | from dlna import DlnaDevices, utils, DlnaDeviceServices 5 | from samsungtv.services.remote_control import RemoteControl 6 | 7 | VERSION = "1.0" 8 | 9 | 10 | class SamsungTVAction(object): 11 | 12 | def __init__(self, label, result=None): 13 | self.label = label 14 | self.result = result 15 | pass 16 | 17 | def __repr__(self): 18 | return self.label 19 | 20 | 21 | class SamsungTVActionList(object): 22 | 23 | def __init__(self, label, result=None, fields=[]): 24 | self.label = label 25 | self.result = result 26 | self.fields = fields 27 | 28 | pass 29 | 30 | def __repr__(self): 31 | 32 | res = "\n" 33 | res += self.label + "\n" 34 | for item in self.result: 35 | 36 | if isinstance(item, dict): 37 | for field in self.fields: 38 | res += item[field] + " " 39 | pass 40 | elif isinstance(item, str): 41 | res += item 42 | else: 43 | raise Exception("Unknown item type {}".format(type(item))) 44 | 45 | res += "\n" 46 | 47 | return res 48 | 49 | 50 | class SamsungTvApp(object): 51 | """docstring for SamsungTvApp""" 52 | 53 | def __init__(self): 54 | 55 | self.host = utils.detect_ip_address() 56 | self.port = 8000 57 | self.name = u'SamsungTvApp' 58 | self.uri = "http://" + self.host + ":" + str(self.port) 59 | self.app_ip = utils.detect_ip_address() 60 | self.volume_step = 2 61 | 62 | devices = DlnaDevices("devices") 63 | 64 | tv_device = devices.get_device_by_type(DlnaDevices.MEDIA_RENDERER) 65 | dial_device = devices.get_device_by_type(DlnaDevices.DIAL_RECEIVER) 66 | 67 | if not tv_device: 68 | print "Unable to find a tv device in local network" 69 | devices.clean() # Cleanup if cache 70 | exit(1) 71 | 72 | if not dial_device: 73 | print "Unable to find a ttv dial device in local network" 74 | devices.clean() # Cleanup if cache 75 | exit(1) 76 | 77 | # c = DlnaDeviceServices.subscribe_to_all(tv_device, self.uri) 78 | # print c 79 | # exit(1) 80 | 81 | self.service = DlnaDeviceServices.get_service(tv_device, DlnaDeviceServices.SERVICE_AV) 82 | self.service_rendering = DlnaDeviceServices.get_service(tv_device, DlnaDeviceServices.SERVICE_RC) 83 | self.service_dial = DlnaDeviceServices.get_service(dial_device, DlnaDeviceServices.SERVICE_DIAL) 84 | self.remote_control = RemoteControl("192.168.0.100", name=self.name) 85 | 86 | self.httpctrl = HttpProxyServerCtrl(port=self.port) 87 | 88 | pass 89 | 90 | @staticmethod 91 | def scan(rescan=False): 92 | 93 | print "Scanning ..." 94 | 95 | devices = DlnaDevices("devices") 96 | 97 | for key, device in devices.get_devices(rescan).items(): 98 | print device 99 | # import pprint 100 | # pprint.pprint( device.info) 101 | pass 102 | 103 | def stop_httpd(self, args=None): 104 | 105 | self.httpctrl.stop() 106 | return "Http Server Stopped" 107 | 108 | def start_httpd(self, args=None): 109 | self.httpctrl.start() 110 | 111 | print "Http Server Started" 112 | 113 | def file(self, arg): 114 | self.start_httpd() 115 | 116 | time.sleep(2) 117 | 118 | url = arg 119 | print url 120 | self.service.url(url) 121 | 122 | def add_file(self, arg): 123 | 124 | return SamsungTVAction( 125 | "Added URL %s" % (arg), 126 | self.service.set_next_url(arg) 127 | ) 128 | 129 | def play(self, arg): 130 | return SamsungTVAction( 131 | "Play %s" % (arg), 132 | self.service.play() 133 | ) 134 | 135 | def stop(self, arg): 136 | return SamsungTVAction( 137 | "Stop %s" % (arg), 138 | self.service.stop() 139 | ) 140 | 141 | def next(self, arg): 142 | return SamsungTVAction( 143 | "Next %s" % (arg), 144 | self.service.next() 145 | ) 146 | 147 | def prev(self, arg): 148 | return SamsungTVAction( 149 | "Stop %s" % (arg), 150 | self.service.previous() 151 | ) 152 | 153 | def volume(self, arg): 154 | return SamsungTVAction( 155 | "Set Volime to %s" % (arg), 156 | self.service_rendering.volume(arg) 157 | ) 158 | 159 | def volup(self, arg): 160 | vol = str(self.service_rendering.volume() + self.volume_step) 161 | 162 | return SamsungTVAction( 163 | "Incr Volime to %s" % (vol), 164 | self.service_rendering.volume(vol) 165 | ) 166 | 167 | def voldown(self, arg): 168 | vol = str(self.service_rendering.volume() - self.volume_step) 169 | 170 | return SamsungTVAction( 171 | "Decr Volime to %s" % (vol), 172 | self.service_rendering.volume(vol) 173 | ) 174 | 175 | def mute(self, arg): 176 | return SamsungTVAction( 177 | "Set Mute On", 178 | self.service_rendering.mute(True) 179 | ) 180 | 181 | def unmute(self, arg): 182 | return SamsungTVAction( 183 | "Set Mute Off", 184 | self.service_rendering.mute(False) 185 | ) 186 | 187 | def app_on(self, arg): 188 | return SamsungTVAction( 189 | "Start App %s " % (arg), 190 | self.service_dial.start(arg) 191 | ) 192 | 193 | def app_off(self, arg): 194 | return SamsungTVAction( 195 | "Stop App %s " % (arg), 196 | self.service_dial.stop(arg) 197 | ) 198 | 199 | def app(self, arg): 200 | print "App %s " % (arg) 201 | print self.service_dial.get(arg) 202 | return "" 203 | 204 | def app_install(self, arg): 205 | return SamsungTVAction( 206 | "Install App %s " % (arg), 207 | self.service_dial.install(arg) 208 | ) 209 | 210 | def keys(self, arg): 211 | 212 | return SamsungTVActionList( 213 | "Keys \n", 214 | RemoteControl.KEY_CODES 215 | ) 216 | 217 | def key(self, arg): 218 | """ 219 | 220 | :type arg: str 221 | """ 222 | 223 | if arg.find(",") > 0: 224 | key_codes = arg.split(",") 225 | 226 | self.remote_control.connect() 227 | for key_code in key_codes: 228 | # self.key(key_code) 229 | self.remote_control.command(key_code) 230 | self.remote_control.close() 231 | 232 | return SamsungTVAction( 233 | "Keys Pressed \n " + 234 | "\n ".join(key_codes) 235 | ) 236 | else: 237 | self.remote_control.connect() 238 | result = self.remote_control.command(arg) 239 | self.remote_control.close() 240 | 241 | return SamsungTVAction( 242 | "Key Pressed %s" % arg, 243 | result 244 | ) 245 | 246 | def launch(self, arg): 247 | 248 | self.remote_control.connect() 249 | result = self.remote_control.launch(arg) 250 | self.remote_control.close() 251 | 252 | return SamsungTVAction( 253 | "App Launched %s" % arg, 254 | result 255 | ) 256 | 257 | def apps(self, arg=None): 258 | 259 | self.remote_control.connect() 260 | app_list = self.remote_control.apps() 261 | self.remote_control.close() 262 | 263 | return SamsungTVActionList("Apps \n", app_list, ["appId", "name"]) 264 | 265 | def run(self, method, arg): 266 | return self.__getattribute__(method)(arg) 267 | -------------------------------------------------------------------------------- /samsungtv/dlna/__init__.py: -------------------------------------------------------------------------------- 1 | """DLNA Services""" 2 | 3 | from .device import DlnaDevice 4 | from .device_services import DlnaDeviceServices 5 | from .devices import DlnaDevices 6 | from .utils import Cache 7 | 8 | __all__ = [ 9 | 'DlnaDevice', 10 | 'DlnaDevices', 11 | 'DlnaDeviceServices', 12 | 'Cache' 13 | ] 14 | -------------------------------------------------------------------------------- /samsungtv/dlna/device.py: -------------------------------------------------------------------------------- 1 | import xml.etree.cElementTree as XML 2 | import requests 3 | import re 4 | import urlparse 5 | 6 | # from dlna import utils 7 | from samsungtv.dlna import utils 8 | from utils import etree_to_dict 9 | 10 | 11 | class DlnaDevice(object): 12 | 13 | def __init__(self, location=""): 14 | parsed = urlparse.urlparse(location) 15 | 16 | self.ip = parsed.hostname 17 | self.port = parsed.port 18 | self.path = parsed.path 19 | self.location = location 20 | self.name = "N/A" 21 | self.applicationUrl = None 22 | 23 | data = self.__get_data(location) 24 | 25 | self.info = data['root']['device'] 26 | self.name = self.info['friendlyName'] 27 | self.applicationUrl = data['headers'].get('application-url') 28 | 29 | self.services = {} 30 | 31 | if type(data['root']['device']['serviceList']['service']) is list: 32 | for service in data['root']['device']['serviceList']['service']: 33 | self.services[service["serviceType"]] = service 34 | else: 35 | service = data['root']['device']['serviceList']['service'] 36 | self.services[service["serviceType"]] = service 37 | 38 | del self.info['serviceList'] 39 | del data 40 | 41 | self.info = utils.clean_ns_from_list(self.info) 42 | 43 | def __get_data(self, location): 44 | r = requests.get(location) 45 | 46 | xml_string = r.content 47 | xml_string = re.sub(' xmlns="[^"]+"', '', xml_string, count=1) 48 | 49 | xml = XML.fromstring(xml_string) 50 | data = etree_to_dict(xml) 51 | data['headers'] = r.headers 52 | 53 | return data 54 | 55 | 56 | def __repr__(self): 57 | res = "" 58 | res += self.name + " [ " + str(self.info["modelName"]) + ", " \ 59 | + str(self.info["modelDescription"]) + " ] @ " + self.location + "\n" 60 | 61 | return res 62 | -------------------------------------------------------------------------------- /samsungtv/dlna/device_services.py: -------------------------------------------------------------------------------- 1 | from samsungtv.services.dial import DialService 2 | from samsungtv.upnpevents import EventSubscriber 3 | from samsungtv.upnpservice import UPnPServiceAVTransport, UPnPServiceRendering, UPnPServiceConnectionManager 4 | 5 | 6 | class DlnaDeviceServices: 7 | SERVICE_AV = "urn:schemas-upnp-org:service:AVTransport:1" 8 | SERVICE_RC = "urn:schemas-upnp-org:service:RenderingControl:1" 9 | SERVICE_CM = "urn:schemas-upnp-org:service:ConnectionManager:1" 10 | SERVICE_DIAL = "dial" 11 | 12 | @staticmethod 13 | def get_service(device, type): 14 | 15 | if device.services.get(type) is None and type is not DlnaDeviceServices.SERVICE_DIAL: 16 | raise Exception("Unsupported service {}".format(type)) 17 | 18 | if type == DlnaDeviceServices.SERVICE_AV: 19 | 20 | return UPnPServiceAVTransport(device.ip, device.port, config=device.services[type]) 21 | 22 | elif type == DlnaDeviceServices.SERVICE_RC: 23 | 24 | return UPnPServiceRendering(device.ip, device.port, config=device.services[type]) 25 | elif type == DlnaDeviceServices.SERVICE_CM: 26 | 27 | return UPnPServiceConnectionManager(device.ip, device.port, config=device.services[type]) 28 | elif type == DlnaDeviceServices.SERVICE_DIAL: 29 | 30 | if device.applicationUrl is not None and device.applicationUrl != "": 31 | return DialService(device.applicationUrl) 32 | else: 33 | raise Exception("Device does not support service ()".format(type)) 34 | 35 | else: 36 | raise Exception("Unsupported service ()".format(type)) 37 | 38 | @staticmethod 39 | def get_event_subscriber(device, type, callback=""): 40 | 41 | if device.services.get(type): 42 | url = "http://{}:{}{}".format(device.ip, device.port, device.services[type]['eventSubURL']) 43 | 44 | print "========" 45 | print device.services[type] 46 | print url 47 | 48 | return EventSubscriber(url, callback) 49 | 50 | else: 51 | raise Exception("Unsupported event service ()".format(type)) 52 | 53 | pass 54 | 55 | @staticmethod 56 | def get_event_subscribers(device, callback=""): 57 | 58 | subscribers = {} 59 | 60 | print device.info 61 | 62 | for stype, service in device.services.items(): 63 | if service.get('eventSubURL'): 64 | subscribers[stype] = DlnaDeviceServices.get_event_subscriber(device, stype, callback) 65 | 66 | return subscribers 67 | 68 | @staticmethod 69 | def subscribe(device, type, callback=""): 70 | subscriber = DlnaDeviceServices.get_event_subscriber(device, type, callback) 71 | subscriber.subscribe() 72 | 73 | @staticmethod 74 | def subscribe_to_all(device, callback=""): 75 | 76 | for stype, subscriber in DlnaDeviceServices.get_event_subscribers(device, callback).items(): 77 | print subscriber 78 | subscriber.subscribe() 79 | -------------------------------------------------------------------------------- /samsungtv/dlna/devices.py: -------------------------------------------------------------------------------- 1 | from utils import Cache 2 | from samsungtv.services.ssdp import SSDPDiscovery 3 | from device import DlnaDevice 4 | 5 | 6 | class DlnaDevices(object): 7 | 8 | MEDIA_RENDERER = "urn:schemas-upnp-org:device:MediaRenderer:1" 9 | DIAL_RECEIVER = "urn:dial-multiscreen-org:device:dialreceiver:1" 10 | 11 | def __init__(self, cache=None): 12 | self.cache = cache 13 | 14 | pass 15 | 16 | def _get_devices(self): 17 | discovery = SSDPDiscovery() 18 | result = discovery.discover(SSDPDiscovery.ST_ALL) 19 | devices = {} 20 | 21 | for headers in result: 22 | devices[headers['location']] = DlnaDevice(headers['location']) 23 | 24 | return devices 25 | 26 | def get_devices(self, refresh=False): 27 | 28 | if self.cache: 29 | if refresh is True or not Cache.get(self.cache): 30 | result = self._get_devices() 31 | 32 | Cache.set(self.cache, result) 33 | else: 34 | result = Cache.get(self.cache) 35 | else: 36 | result = self._get_devices() 37 | 38 | return result 39 | 40 | def get_device_by_type(self, dtype=""): 41 | # type: (dev) -> DlnaDevice 42 | """ 43 | 44 | :param dtype: 45 | :return DlnaDevice: Device obj 46 | """ 47 | for key, dev in self.get_devices().items(): 48 | if dev.info['deviceType'] == dtype: 49 | return dev 50 | return None 51 | 52 | def clean(self): 53 | if self.cache: 54 | Cache.clear(self.cache) 55 | 56 | -------------------------------------------------------------------------------- /samsungtv/dlna/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pickle 3 | import re 4 | import socket 5 | from collections import defaultdict 6 | 7 | 8 | class Cache(object): 9 | path = './cache' 10 | 11 | @staticmethod 12 | def set(name, value): 13 | file = '%s/%s.pickle' % (Cache.path, name) 14 | 15 | with open(file, 'wb') as handle: 16 | pickle.dump(value, handle, protocol=pickle.HIGHEST_PROTOCOL) 17 | 18 | pass 19 | 20 | @staticmethod 21 | def get(name): 22 | value = None 23 | file = '%s/%s.pickle' % (Cache.path, name) 24 | 25 | if os.path.isfile(file): 26 | with open(file, 'rb') as handle: 27 | value = pickle.load(handle) 28 | 29 | return value 30 | 31 | @staticmethod 32 | def clear(name): 33 | file = '%s/%s.pickle' % (Cache.path, name) 34 | 35 | if os.path.isfile(file): 36 | os.remove(file) 37 | 38 | 39 | def detect_ip_address(): 40 | """Return the local ip-address""" 41 | # https://stackoverflow.com/a/166589 42 | 43 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 44 | s.connect(("8.8.8.8", 80)) 45 | ip_address = s.getsockname()[0] 46 | s.close() 47 | 48 | return ip_address 49 | 50 | 51 | def clean_ns_from_list(data=None): 52 | if data is None: 53 | data = {} 54 | 55 | result = {} 56 | if data: 57 | for key, el in data.items(): 58 | key = re.sub('{[^{}]+}', '', key, count=1) 59 | result[key] = el 60 | 61 | return result 62 | 63 | 64 | def etree_to_dict(t): 65 | d = {t.tag: {} if t.attrib else None} 66 | children = list(t) 67 | if children: 68 | dd = defaultdict(list) 69 | for dc in map(etree_to_dict, children): 70 | for k, v in dc.items(): 71 | dd[k].append(v) 72 | d = {t.tag: {k: v[0] if len(v) == 1 else v for k, v in dd.items()}} 73 | if t.attrib: 74 | d[t.tag].update(('@' + k, v) for k, v in t.attrib.items()) 75 | if t.text: 76 | text = t.text.strip() 77 | if children or t.attrib: 78 | if text: 79 | d[t.tag]['#text'] = text 80 | else: 81 | d[t.tag] = text 82 | return d 83 | -------------------------------------------------------------------------------- /samsungtv/httpd/__init__.py: -------------------------------------------------------------------------------- 1 | """UPnP Services""" 2 | from serverctrl import HttpProxyServerCtrl 3 | 4 | __all__ = [ 5 | 'HttpProxyServerCtrl' 6 | ] 7 | -------------------------------------------------------------------------------- /samsungtv/httpd/composite_handler.py: -------------------------------------------------------------------------------- 1 | from BaseHTTPServer import HTTPServer 2 | 3 | import sys 4 | import os 5 | 6 | from proxy_handler import ProxyHttpRequestHandler 7 | from subscribe_handler import SubscribeHttpRequestHandler 8 | 9 | 10 | class CompositeHttpRequestHandler(ProxyHttpRequestHandler, SubscribeHttpRequestHandler): 11 | pass 12 | 13 | 14 | if __name__ == "__main__": 15 | 16 | host = "" 17 | port = 8008 18 | 19 | try: 20 | CompositeHttpRequestHandler.dir_path = os.path.dirname(os.path.realpath(__file__)) + "/../media" 21 | httpd = HTTPServer((host, port), CompositeHttpRequestHandler) 22 | 23 | except Exception as e: 24 | sys.stderr.write(str(e)) 25 | sys.exit(-1) 26 | 27 | print "Serving on " + host + ":" + str(port) + " ... " 28 | 29 | while True: 30 | try: 31 | httpd.handle_request() 32 | except KeyboardInterrupt as e1: 33 | print "Bye" 34 | sys.exit(0) 35 | -------------------------------------------------------------------------------- /samsungtv/httpd/proxy_handler.py: -------------------------------------------------------------------------------- 1 | import mimetypes 2 | import os 3 | import shutil 4 | from BaseHTTPServer import BaseHTTPRequestHandler 5 | from urllib2 import urlopen 6 | 7 | 8 | class ProxyHttpRequestHandler(BaseHTTPRequestHandler): 9 | 10 | dir_path = os.path.dirname(os.path.realpath(__file__)) + "/" 11 | 12 | def _send_ok(self): 13 | url = self.path[1:] # replace '/' 14 | 15 | file = self.dir_path + "/" + url 16 | 17 | if os.path.exists(file) and os.path.isfile(file): 18 | f = open(file) 19 | content_type = mimetypes.guess_type(file)[0] 20 | 21 | f.close() 22 | elif url.startswith("http"): 23 | f = urlopen(url=url) 24 | content_type = f.info().getheaders("Content-Type")[0] 25 | 26 | f.close() 27 | else: 28 | self.send_response(404, "Not Found") 29 | self.send_header("X-App-Url", url) 30 | self.send_header("X-App-File", file) 31 | self.end_headers() 32 | return None 33 | 34 | self.send_response(200, "ok") 35 | self.send_header('Access-Control-Allow-Origin', '*') 36 | self.send_header('Access-Control-Allow-Methods', 'GET, OPTIONS') 37 | self.send_header("Access-Control-Allow-Headers", "X-Requested-With") 38 | self.send_header("Access-Control-Allow-Headers", "Content-Type") 39 | self.send_header("Content-Type", content_type) 40 | self.send_header("X-App-Url", url) 41 | self.send_header("X-App-File", file) 42 | self.end_headers() 43 | 44 | def do_OPTIONS(self): 45 | self._send_ok() 46 | 47 | def do_HEAD(self): 48 | self._send_ok() 49 | 50 | def do_GET(self): 51 | url = self.path[1:] # replace '/' 52 | 53 | file = self.dir_path + "/" + url 54 | 55 | if os.path.exists(file) and os.path.isfile(file): 56 | f = open(file) 57 | content_type = mimetypes.guess_type(file)[0] 58 | size = os.path.getsize(file) 59 | name = os.path.basename(file) 60 | elif url.startswith("http"): 61 | f = urlopen(url=url) 62 | content_type = f.info().getheaders("Content-Type")[0] 63 | size = f.info().getheaders("Content-Length")[0] 64 | name = os.path.basename(url) 65 | else : 66 | self.send_response(404, "Not Found") 67 | self.end_headers() 68 | return None 69 | 70 | try: 71 | self.send_response(200) 72 | self.send_header('Access-Control-Allow-Origin', '*') 73 | self.send_header("Content-Type", content_type) 74 | self.send_header("Content-Disposition", 'attachment; filename="{}"'.format(name)) 75 | self.send_header("Content-Length", str(size)) 76 | self.end_headers() 77 | 78 | shutil.copyfileobj(f, self.wfile) 79 | finally: 80 | f.close() -------------------------------------------------------------------------------- /samsungtv/httpd/server.py: -------------------------------------------------------------------------------- 1 | import getopt 2 | import os 3 | import sys 4 | from BaseHTTPServer import HTTPServer 5 | 6 | from composite_handler import CompositeHttpRequestHandler 7 | 8 | def usage(): 9 | print "" 10 | print "Usage: " + sys.argv[0] + " [OPTIONS]" 11 | print " -h HOST" 12 | print " -p Port" 13 | print "" 14 | 15 | 16 | if __name__ == "__main__": 17 | 18 | host = "" 19 | port = 8000 20 | 21 | try: 22 | opts, args = getopt.getopt(sys.argv[1:], "h:p:", ["host", "port"]) 23 | except getopt.GetoptError, err: 24 | print(err) 25 | sys.exit(-1) 26 | 27 | for o, arg in opts: 28 | if o in ("-h", "--host"): 29 | host = arg 30 | elif o in ("-p", "--port"): 31 | port = int(arg) 32 | 33 | else: 34 | print "Unknown Options" 35 | 36 | try: 37 | CompositeHttpRequestHandler.dir_path = os.curdir + "/media" 38 | # CompositeHttpRequestHandler.dir_path = os.path.dirname(os.path.realpath(__file__)) + "/../media" 39 | httpd = HTTPServer((host, port), CompositeHttpRequestHandler) 40 | 41 | except Exception as e: 42 | sys.stderr.write(str(e)) 43 | sys.exit(-1) 44 | 45 | print "Serving on " + host + ":" + str(port) + " ... " 46 | 47 | while True: 48 | httpd.handle_request() 49 | -------------------------------------------------------------------------------- /samsungtv/httpd/serverctrl.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | logger = logging.getLogger(__name__) 5 | 6 | 7 | class HttpProxyServerCtrl(object): 8 | 9 | def __init__(self, host="", port=8000, pid_file=""): 10 | """ 11 | Httpd Control 12 | :type port: int 13 | :type host: str 14 | """ 15 | 16 | dir_path = os.path.dirname(os.path.realpath(__file__)) 17 | 18 | self.host = host 19 | self.port = port 20 | self.pid_file = pid_file if pid_file is not "" else "./stvpid" 21 | self.server = "{0}/server.py".format(dir_path) 22 | 23 | logger.info('pid {}'.format(self.pid_file)) 24 | logger.info('server {}'.format(self.server)) 25 | 26 | def stop(self): 27 | if os.path.isfile(self.pid_file): 28 | cmd = "cat {} | xargs kill && rm {}".format(self.pid_file, self.pid_file) 29 | os.system(cmd) 30 | 31 | pass 32 | 33 | def start(self): 34 | self.stop() 35 | 36 | server_cmd = self.server 37 | 38 | if self.host is not "" : 39 | server_cmd += " -h " + self.host 40 | 41 | if self.port : 42 | server_cmd += " -p " + str(self.port) 43 | 44 | cmd = "nohup python {} & echo $! > {}".format(server_cmd, self.pid_file) 45 | 46 | os.system(cmd) 47 | 48 | logger.debug('cmd {}'.format(cmd)) 49 | logger.info("Http Server Started @ {0}:{1}".format(self.host, str(self.port))) 50 | 51 | def get_pid(self): 52 | pid = "" 53 | 54 | if os.path.isfile(self.pid_file): 55 | with open(self.pid_file, 'r') as handle: 56 | pid = handle.read(1024) 57 | 58 | handle.close() 59 | 60 | return pid 61 | 62 | 63 | if __name__ == "__main__": 64 | 65 | logging.basicConfig(level=logging.DEBUG) 66 | 67 | httpdctrl = HttpProxyServerCtrl() 68 | 69 | httpdctrl.get_pid() 70 | httpdctrl.start() 71 | print httpdctrl.get_pid() 72 | httpdctrl.stop() 73 | -------------------------------------------------------------------------------- /samsungtv/httpd/subscribe_handler.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer 4 | from xml.etree import cElementTree 5 | 6 | from collections import defaultdict 7 | 8 | 9 | def etree_to_dict(t): 10 | d = {t.tag: {} if t.attrib else None} 11 | children = list(t) 12 | if children: 13 | dd = defaultdict(list) 14 | for dc in map(etree_to_dict, children): 15 | for k, v in dc.items(): 16 | dd[k].append(v) 17 | d = {t.tag: {k: v[0] if len(v) == 1 else v for k, v in dd.items()}} 18 | if t.attrib: 19 | d[t.tag].update(('@' + k, v) for k, v in t.attrib.items()) 20 | if t.text: 21 | text = t.text.strip() 22 | if children or t.attrib: 23 | if text: 24 | d[t.tag]['#text'] = text 25 | else: 26 | d[t.tag] = text 27 | return d 28 | 29 | 30 | class SubscribeHttpRequestHandler(BaseHTTPRequestHandler): 31 | NS = "{urn:schemas-upnp-org:event-1-0}" 32 | 33 | def do_NOTIFY(self): 34 | 35 | result = {} 36 | ip, _ = self.client_address 37 | 38 | content_len = int(self.headers.get('content-length', 0)) 39 | data = self.rfile.read(content_len) 40 | 41 | # print "RAWWWWWWWWWWWWWWWWWWWWWWWWWWW" 42 | # print data 43 | 44 | properties = {} 45 | event = {} 46 | if data: 47 | doc = cElementTree.fromstring(data) 48 | 49 | for propnode in doc.findall('./{0}property'.format(self.NS)): 50 | for prop in propnode.getchildren(): 51 | # "Raw" Properties 52 | properties[prop.tag] = prop.text 53 | 54 | if prop.text is not None and prop.text.startswith("<"): 55 | # // Extract Event 56 | xml_string = re.sub(' xmlns="[^"]+"', '', prop.text, count=1) 57 | pxml = cElementTree.fromstring(xml_string) 58 | # pp = pxml.findall('./InstanceID/*') 59 | # event = etree_to_dict(pp) 60 | 61 | event = {} 62 | for node in pxml.findall('./InstanceID/*'): 63 | event[node.tag] = node.attrib 64 | 65 | # print "--------------------------------------" 66 | # print cElementTree.tostring(pxml) 67 | # print cElementTree.tostring(pp) 68 | # print etree_to_dict(pxml) 69 | # print "--------------------------------------" 70 | 71 | result["ip"] = ip 72 | result["sid"] = self.headers.getheader("sid") 73 | result["nt"] = self.headers.getheader("nt") 74 | result["nts"] = self.headers.getheader("nts") 75 | result["event"] = event 76 | 77 | # result["raw"] = { 78 | # result["properties"] = properties 79 | # "data": data, 80 | # "headers": self.headers.items() 81 | # } 82 | 83 | self._log(result) 84 | 85 | response = "