├── news └── .gitignore ├── requirements.txt ├── src └── spotify_local │ ├── config.py │ ├── __init__.py │ ├── utils.py │ └── core.py ├── pyproject.toml ├── .vscode └── settings.json ├── Pipfile ├── tests └── test_spotify_local.py ├── docs ├── Makefile ├── make.bat └── source │ ├── _templates │ ├── hacks.html │ ├── sidebarintro.html │ └── sidebarlogo.html │ ├── index.rst │ └── conf.py ├── CHANGELOG.rst ├── LICENSE ├── .gitignore ├── README.rst ├── setup.py └── Pipfile.lock /news/.gitignore: -------------------------------------------------------------------------------- 1 | !.gitignore -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erinxocon/spotify-local/HEAD/requirements.txt -------------------------------------------------------------------------------- /src/spotify_local/config.py: -------------------------------------------------------------------------------- 1 | DEFAULT_ORIGIN = {"Origin": "https://open.spotify.com"} 2 | DEFAULT_PORT = 4381 3 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.towncrier] 2 | package = "spotify_local" 3 | package_dir = "src" 4 | filename = "CHANGELOG.rst" 5 | directory = "news/" 6 | title_format = "{version} ({project_date})" 7 | 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": 3 | "C:\\Users\\erin\\.virtualenvs\\spotipy-local-Jy9ZZi5M\\Scripts\\python.exe", 4 | "python.linting.pylintEnabled": false, 5 | "python.formatting.provider": "black" 6 | } 7 | -------------------------------------------------------------------------------- /src/spotify_local/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | try: 4 | assert sys.version_info.major == 3 5 | assert sys.version_info.minor > 2 6 | except AssertionError: 7 | raise RuntimeError("Spotify-Local requires Python 3.3+!") 8 | 9 | from .core import SpotifyLocal 10 | from .utils import ( 11 | is_spotify_running, 12 | is_spotify_web_helper_running, 13 | start_spotify_web_helper, 14 | start_spotify, 15 | ) 16 | 17 | __version__ = "0.3.2" 18 | 19 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | requests = "*" 8 | keyboard = "*" 9 | pyobjc-framework-Quartz = {version="*", markers="sys_platform == 'dawrin'"} 10 | psutil = "*" 11 | "delegator.py" = "*" 12 | 13 | [dev-packages] 14 | spotify-local = {editable = true, path = "R:/spotipy-local"} 15 | mypy = "*" 16 | rope = "*" 17 | twine = "*" 18 | white = "*" 19 | sphinx = "*" 20 | towncrier = "*" 21 | 22 | [requires] 23 | python_version = "3.6" 24 | -------------------------------------------------------------------------------- /tests/test_spotify_local.py: -------------------------------------------------------------------------------- 1 | from spotify_local import SpotifyLocal, is_spotify_web_helper_running 2 | from time import sleep 3 | 4 | print(is_spotify_web_helper_running()) 5 | s = SpotifyLocal() 6 | 7 | 8 | @s.on_play_state_change 9 | def test(status): 10 | print("Play Pause Engaged") 11 | 12 | 13 | @s.on_status_change 14 | def test1(status): 15 | print("new status") 16 | 17 | 18 | @s.on_track_change 19 | def test2(status): 20 | print("track changed") 21 | 22 | 23 | print(s._registered_events) 24 | 25 | s.listen(blocking=True) 26 | 27 | print("let's see if this prints out") 28 | 29 | sleep(120) 30 | 31 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = spotify-local-control 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | set SPHINXPROJ=spotify-local-control 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | 0.3.2 (2018-06-21) 2 | ================== 3 | 4 | Features 5 | -------- 6 | 7 | - You can now check if spotify and it's web helper are running and if not start 8 | them! (#2) 9 | - Added decorators for track_change, status_change, and play_state_change, so 10 | the user doesn't have to type them out manually (#3) 11 | 12 | 13 | 0.3.0 (2018-06-20) 14 | ================== 15 | 16 | Features 17 | -------- 18 | 19 | - Added a new class that allows for async operations using asyncio (#0) 20 | - Changed project name from "Spotify-Local-Control" to "Spotify-Local" (#1) 21 | - `listen_for_events()` is now blocking, but uses new @on dectorator to call 22 | functions like in node.js (#2) 23 | - You can now listen for three seperate events, `play_state_change`, 24 | `track_change`, and `status_change` (#3) 25 | - `listen_for_events` can now be blocking or non-blocking (#4) 26 | - refactored to use event emiiters! (#5) 27 | 28 | 29 | 0.2.3 (2018-06-15) 30 | ================== 31 | 32 | Improved Documentation 33 | ---------------------- 34 | 35 | - Added doc strings to most functions (#0) 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Erin O'Connell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/source/_templates/hacks.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /docs/source/_templates/sidebarintro.html: -------------------------------------------------------------------------------- 1 |

2 | 4 |

5 | 6 |

7 | Spotify Local Control intends to make controlling the local spotify web helper as simple and intuitive as possible. 8 |

9 | 10 |

Stay Informed

11 | 12 |

13 | 15 |

16 | 17 |

18 | Follow @kennethreitz 19 | 20 |

21 |

22 | Say Thanks! 23 |

24 | 25 |

Other Projects

26 | 27 |

More Erin O'Connell projects:

28 | -------------------------------------------------------------------------------- /docs/source/_templates/sidebarlogo.html: -------------------------------------------------------------------------------- 1 | 6 | 7 |

8 | 10 |

11 | 12 |

13 | Requests-XML intends to make parsing XML documents as simple and intuitive as possible. 14 |

15 | 16 |

Stay Informed

17 | 18 |

19 | 21 |

22 | 23 |

24 | Follow @kennethreitz 25 | 26 |

27 |

28 | Say Thanks! 29 |

30 | 31 |

Other Projects

32 | 33 |

More Erin O'Connell projects:

34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | **Spotify has replaced most of this functionality with the web api. This project is no longer maintained!** 2 | 3 | 4 | 5 | Spotify-Local: A multi-platform API to control the local Spotify Client 6 | =============================================================================== 7 | 8 | .. image:: https://img.shields.io/pypi/v/spotify-local.svg?maxAge=2592000 9 | :target: https://pypi.python.org/pypi/spotify-local/ 10 | .. image:: https://img.shields.io/pypi/l/spotify-local.svg?maxAge=2592000 11 | :target: https://opensource.org/licenses/MIT 12 | 13 | **Spotify-Local** library is designed to make controlling the Spotify client on your local machine possible! 14 | This is a wrapper for the web helper process which exposes a simple api. 15 | **Spotify-Local** is inspired by `SpotifyAPI-NET `_. 16 | This library allows you to perform simple actions quickly, or listen to events and register callbacks when 17 | a song changes, or the pause button is pushed. 18 | 19 | When using this library you automatically get: 20 | 21 | - The ability to play/pause the current song 22 | - The ability to change tracks 23 | - You can register callbacks and listen for events when the state of Spotify changes 24 | - A nice context manager api using `with` 25 | 26 | 27 | Installation 28 | ============ 29 | 30 | .. code-block:: shell 31 | 32 | $ pipenv install spotify-local 33 | 34 | Only **Python 3.3+** is supported. 35 | 36 | 37 | Tutorial & Usage 38 | ================ 39 | 40 | Connect to the Spotify Client (Spotify must be open to do this): 41 | 42 | .. code-block:: pycon 43 | 44 | >>> from spotify_local import SpotifyLocal 45 | 46 | >>> with SpotifyLocal() as s: 47 | pass 48 | 49 | Pause the Spotify Client: 50 | 51 | .. code-block:: pycon 52 | 53 | >>> with SpotifyLocal() as s: 54 | s.pause() 55 | 56 | 57 | Grab the current state of the Spotify client, including now playing information: 58 | 59 | .. code-block:: pycon 60 | 61 | >>> with SpotifyLocal() as s: 62 | print(s.get_current_status()) 63 | 64 | Play a playlist, song, album, artist, etc using a Spotify uri link: 65 | 66 | .. code-block:: pycon 67 | 68 | >>> with SpotifyLocal() as s: 69 | s.playURI('spotify:track:0thLhIqWsqqycEqFONOyhu') 70 | 71 | Register a callback and listen for events: 72 | 73 | .. code-block:: pycon 74 | 75 | >>> from spotify_local import SpotifyLocal 76 | >>> s = SpotifyLocal() 77 | >>> @s.on('track_change') 78 | >>> def test(event): 79 | ... print(event) 80 | >>> s.listen(blocking=False) 81 | >>> print("Do more stuff because that runs in the background") 82 | 83 | 84 | License 85 | ======= 86 | MIT 87 | 88 | TODO 89 | ==== 90 | * Create an async verion of the spotify controller class 91 | -------------------------------------------------------------------------------- /src/spotify_local/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import psutil 5 | import delegator 6 | 7 | from random import choices 8 | from string import ascii_lowercase 9 | 10 | from requests import Response, session, Session 11 | 12 | from .config import DEFAULT_PORT, DEFAULT_ORIGIN 13 | 14 | s: Session = Session() 15 | 16 | 17 | def get_url(url): 18 | """Ranomdly generates a url for use in requests. 19 | Generates a hostname with the port and the provided suffix url provided 20 | 21 | :param url: A url fragment to use in the creation of the master url 22 | """ 23 | sub = "{0}.spotilocal.com".format("".join(choices(ascii_lowercase, k=10))) 24 | return "http://{0}:{1}{2}".format(sub, DEFAULT_PORT, url) 25 | 26 | 27 | def get_oauth_token(): 28 | """Retrieve a simple OAuth Token for use with the local http client.""" 29 | url = "{0}/token".format(DEFAULT_ORIGIN["Origin"]) 30 | r = s.get(url=url) 31 | return r.json()["t"] 32 | 33 | 34 | def get_csrf_token(): 35 | """Retrieve a simple csrf token for to prevent cross site request forgery.""" 36 | url = get_url("/simplecsrf/token.json") 37 | r = s.get(url=url, headers=DEFAULT_ORIGIN) 38 | return r.json()["token"] 39 | 40 | 41 | def is_spotify_running(): 42 | procs = [p.name() for p in psutil.process_iter()] 43 | if sys.platform == "win32": 44 | return "Spotify.exe" in procs 45 | else: 46 | return "Spotify" in procs 47 | 48 | 49 | def is_spotify_web_helper_running(): 50 | procs = [p.name() for p in psutil.process_iter()] 51 | if sys.platform == "win32": 52 | return "SpotifyWebHelper.exe" in procs 53 | else: 54 | return "SpotifyWebHelper" in procs 55 | 56 | 57 | def start_spotify_web_helper(): 58 | # windows: %APPDATA%\Spotify\SpotifyWebHelper.exe 59 | # macOS: /Users/user/Library/Application Support/Spotify/SpotifyWebHelper 60 | 61 | if sys.platform == "win32": 62 | appdata = os.environ["APPDATA"] 63 | path = os.path.join(appdata, "Spotify", "SpotifyWebHelper.exe") 64 | delegator.run(path, block=False) 65 | 66 | elif sys.platform == "darwin": 67 | home = os.environ["HOME"] 68 | path = os.path.join( 69 | home, "Library", "Application Support", "Spotify", "SpotifyWebHelper" 70 | ) 71 | delegator.run(path, block=False) 72 | 73 | 74 | def start_spotify(): 75 | if sys.platform == "win32": 76 | appdata = os.environ["APPDATA"] 77 | path = os.path.join(appdata, "Spotify", "Spotify.exe") 78 | delegator.run(path, block=False) 79 | 80 | # elif sys.platform == "darwin": 81 | # home = os.environ["HOME"] 82 | # path = os.path.join( 83 | # home, "Library", "Application Support", "Spotify", "Spotify" 84 | # ) 85 | # delegator.run(path, block=False) 86 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. spotify-local-control documentation master file, created by 2 | sphinx-quickstart on Tue Feb 27 08:03:45 2018. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Spotify-Local: A multi-platform API to control the local Spotify Client 7 | =============================================================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | **Spotify-Local** library is designed to make controlling the Spotify client on your local machine possible! 14 | This is a wrapper for the web helper process which exposes a simple api. 15 | **Spotify-Local** is inspired by `SpotifyAPI-NET `_. 16 | This library allows you to perform simple actions quickly, or listen to events and register callbacks when 17 | a song changes, or the pause button is pushed. 18 | 19 | When using this library you automatically get: 20 | 21 | - The ability to play/pause the current song 22 | - The ability to change tracks 23 | - You can register callbacks and listen for events when the state of Spotify changes 24 | - A nice context manager api using `with` 25 | 26 | 27 | Installation 28 | ============ 29 | 30 | .. code-block:: shell 31 | 32 | $ pipenv install spotify-local 33 | 34 | Only **Python 3.3+** is supported. 35 | 36 | 37 | Tutorial & Usage 38 | ================ 39 | 40 | Connect to the Spotify Client (Spotify must be open to do this): 41 | 42 | .. code-block:: pycon 43 | 44 | >>> from spotify_local import SpotifyLocal 45 | 46 | >>> with SpotifyLocal() as s: 47 | pass 48 | 49 | Pause the Spotify Client: 50 | 51 | .. code-block:: pycon 52 | 53 | >>> with SpotifyLocal() as s: 54 | s.pause() 55 | 56 | 57 | Grab the current state of the Spotify client, including now playing information: 58 | 59 | .. code-block:: pycon 60 | 61 | >>> with SpotifyLocal() as s: 62 | print(s.get_current_status()) 63 | 64 | Play a playlist, song, album, artist, etc using a Spotify uri link: 65 | 66 | .. code-block:: pycon 67 | 68 | >>> with SpotifyLocal() as s: 69 | s.playURI('spotify:track:0thLhIqWsqqycEqFONOyhu') 70 | 71 | Register a callback and listen for events: 72 | 73 | .. code-block:: pycon 74 | 75 | >>> from spotify_local import SpotifyLocal 76 | >>> s = SpotifyLocal() 77 | >>> @s.on('track_change') 78 | >>> def test(event): 79 | ... print(event) 80 | >>> s.listen(blocking=False) 81 | >>> print("Do more stuff because that runs in the background") 82 | 83 | 84 | License 85 | ======= 86 | MIT 87 | 88 | 89 | API Documentation 90 | ================= 91 | 92 | Main Classes 93 | ------------ 94 | 95 | .. module:: spotify_local 96 | 97 | These classes are the main interface to ``spotify_local``: 98 | 99 | 100 | .. autoclass:: SpotifyLocal 101 | :inherited-members: 102 | 103 | 104 | Indices and tables 105 | ================== 106 | 107 | * :ref:`genindex` 108 | * :ref:`modindex` 109 | * :ref:`search` -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Note: To use the 'upload' functionality of this file, you must: 5 | # $ pip install twine 6 | 7 | import io 8 | import os 9 | import sys 10 | import re 11 | from shutil import rmtree 12 | 13 | from setuptools import setup, Command, find_packages 14 | 15 | 16 | here = os.path.abspath(os.path.dirname(__file__)) 17 | 18 | # Import the README and use it as the long-description. 19 | # Note: this will only work if 'README.rst' is present in your MANIFEST.in file! 20 | with io.open(os.path.join(here, "README.rst"), encoding="utf-8") as f: 21 | long_description = "\n" + f.read() 22 | 23 | 24 | def find_version(*file_paths): 25 | with io.open(os.path.join(here, *file_paths), encoding="utf-8") as f: 26 | version_file = f.read() 27 | version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) 28 | if version_match: 29 | return version_match.group(1) 30 | raise RuntimeError("Unable to find version string.") 31 | 32 | 33 | # Package meta-data. 34 | NAME = "spotify-local" 35 | DESCRIPTION = "Communicate with the Spotify's web helper process to control some basic aspects of spotify" 36 | URL = "https://github.com/erinxocon/spotify-local-control" 37 | EMAIL = "erinocon5@gmail.com" 38 | AUTHOR = "Erin O'Connell" 39 | VERSION = find_version("src", "spotify_local", "__init__.py") 40 | 41 | # What packages are required for this module to be executed? 42 | REQUIRED = [ 43 | "requests", 44 | "keyboard", 45 | "pyobjc-framework-Quartz; sys.platform == 'darwin'", 46 | ] 47 | 48 | 49 | class UploadCommand(Command): 50 | """Support setup.py upload.""" 51 | 52 | description = "Build and publish the package." 53 | user_options = [] # type: ignore 54 | 55 | @staticmethod 56 | def status(s): 57 | """Prints things in bold.""" 58 | print("\033[1m{0}\033[0m".format(s)) 59 | 60 | def initialize_options(self): 61 | pass 62 | 63 | def finalize_options(self): 64 | pass 65 | 66 | def run(self): 67 | try: 68 | self.status("Removing previous builds…") 69 | rmtree(os.path.join(here, "dist")) 70 | except OSError: 71 | pass 72 | 73 | self.status("Building Source and Wheel (universal) distribution…") 74 | os.system("{0} setup.py sdist bdist_wheel --universal".format(sys.executable)) 75 | 76 | self.status("Uploading the package to PyPi via Twine…") 77 | os.system("twine upload dist/*") 78 | 79 | self.status("Publishing git tags…") 80 | os.system("git tag v{0}".format(VERSION)) 81 | os.system("git push --tags") 82 | 83 | sys.exit() 84 | 85 | 86 | # Where the magic happens: 87 | setup( 88 | name=NAME, 89 | version=VERSION, 90 | description=DESCRIPTION, 91 | long_description=long_description, 92 | author=AUTHOR, 93 | author_email=EMAIL, 94 | url=URL, 95 | python_requires=">=3.6.0", 96 | package_dir={"": "src"}, 97 | packages=find_packages(where="src", exclude=["docs", "tests*"]), 98 | install_requires=REQUIRED, 99 | include_package_data=True, 100 | license="MIT", 101 | classifiers=[ 102 | # Trove classifiers 103 | # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers 104 | "License :: OSI Approved :: MIT License", 105 | "Programming Language :: Python", 106 | "Programming Language :: Python :: 3.6", 107 | "Programming Language :: Python :: Implementation :: CPython", 108 | "Programming Language :: Python :: Implementation :: PyPy", 109 | ], 110 | # $ setup.py publish support. 111 | cmdclass={"upload": UploadCommand}, 112 | ) 113 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/stable/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | # import os 16 | # import sys 17 | # sys.path.insert(0, os.path.abspath('.')) 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | import spotify_local 22 | 23 | project = "spotify-local" 24 | copyright = u"2018. An Erin O'Connell Project" 25 | author = "Erin O'Connell" 26 | 27 | # The short X.Y version 28 | version = "" 29 | # The full version, including alpha/beta/rc tags 30 | release = "v0.2.3" 31 | 32 | 33 | # -- General configuration --------------------------------------------------- 34 | 35 | # If your documentation needs a minimal Sphinx version, state it here. 36 | # 37 | # needs_sphinx = '1.0' 38 | 39 | # Add any Sphinx extension module names here, as strings. They can be 40 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 41 | # ones. 42 | extensions = [ 43 | "sphinx.ext.autodoc", 44 | "sphinx.ext.doctest", 45 | "sphinx.ext.intersphinx", 46 | "sphinx.ext.todo", 47 | "sphinx.ext.coverage", 48 | "sphinx.ext.viewcode", 49 | "sphinx.ext.githubpages", 50 | ] 51 | 52 | # Add any paths that contain templates here, relative to this directory. 53 | templates_path = ["_templates"] 54 | 55 | # The suffix(es) of source filenames. 56 | # You can specify multiple suffix as a list of string: 57 | # 58 | # source_suffix = ['.rst', '.md'] 59 | source_suffix = ".rst" 60 | 61 | # The master toctree document. 62 | master_doc = "index" 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | # 67 | # This is also used if you do content translation via gettext catalogs. 68 | # Usually you set "language" from the command line for these cases. 69 | language = None 70 | 71 | # List of patterns, relative to source directory, that match files and 72 | # directories to ignore when looking for source files. 73 | # This pattern also affects html_static_path and html_extra_path . 74 | exclude_patterns = [] # type: ignore 75 | 76 | # The name of the Pygments (syntax highlighting) style to use. 77 | pygments_style = "sphinx" 78 | 79 | 80 | # -- Options for HTML output ------------------------------------------------- 81 | 82 | # The theme to use for HTML and HTML Help pages. See the documentation for 83 | # a list of builtin themes. 84 | # 85 | html_theme = "alabaster" 86 | 87 | # Theme options are theme-specific and customize the look and feel of a theme 88 | # further. For a list of options available for each theme, see the 89 | # documentation. 90 | # 91 | html_theme_options = { 92 | "show_powered_by": False, 93 | "github_user": "erinxocon", 94 | "github_repo": "spotify-local-control", 95 | "github_banner": True, 96 | "show_related": False, 97 | "note_bg": "#FFF59C", 98 | } 99 | 100 | # Add any paths that contain custom static files (such as style sheets) here, 101 | # relative to this directory. They are copied after the builtin static files, 102 | # so a file named "default.css" will overwrite the builtin "default.css". 103 | html_static_path = ["_static"] 104 | 105 | # Custom sidebar templates, must be a dictionary that maps document names 106 | # to template names. 107 | # 108 | # The default sidebars (for documents that don't match any pattern) are 109 | # defined by theme itself. Builtin themes are using these templates by 110 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 111 | # 'searchbox.html']``. 112 | # 113 | html_sidebars = { 114 | "index": ["sidebarintro.html", "sourcelink.html", "searchbox.html", "hacks.html"], 115 | "**": [ 116 | "sidebarlogo.html", 117 | "localtoc.html", 118 | "relations.html", 119 | "sourcelink.html", 120 | "searchbox.html", 121 | "hacks.html", 122 | ], 123 | } 124 | 125 | html_show_sphinx = False 126 | html_show_sourcelink = False 127 | 128 | # -- Options for HTMLHelp output --------------------------------------------- 129 | 130 | # Output file base name for HTML help builder. 131 | htmlhelp_basename = "spotify-local-xmldoc" 132 | 133 | 134 | # -- Options for LaTeX output ------------------------------------------------ 135 | 136 | latex_elements = { # type: ignore 137 | # The paper size ('letterpaper' or 'a4paper'). 138 | # 139 | # 'papersize': 'letterpaper', 140 | # The font size ('10pt', '11pt' or '12pt'). 141 | # 142 | # 'pointsize': '10pt', 143 | # Additional stuff for the LaTeX preamble. 144 | # 145 | # 'preamble': '', 146 | # Latex figure (float) alignment 147 | # 148 | # 'figure_align': 'htbp', 149 | } 150 | 151 | # Grouping the document tree into LaTeX files. List of tuples 152 | # (source start file, target name, title, 153 | # author, documentclass [howto, manual, or own class]). 154 | latex_documents = [ 155 | ( 156 | master_doc, 157 | "requests-xml.tex", 158 | "requests-xml Documentation", 159 | "Erin O'Connell", 160 | "manual", 161 | ) 162 | ] 163 | 164 | 165 | # -- Options for manual page output ------------------------------------------ 166 | 167 | # One entry per manual page. List of tuples 168 | # (source start file, name, description, authors, manual section). 169 | man_pages = [(master_doc, "spotify-local", "spotify-local Documentation", [author], 1)] 170 | 171 | 172 | # -- Options for Texinfo output ---------------------------------------------- 173 | 174 | # Grouping the document tree into Texinfo files. List of tuples 175 | # (source start file, target name, title, author, 176 | # dir menu entry, description, category) 177 | texinfo_documents = [ 178 | ( 179 | master_doc, 180 | "spotify-local", 181 | "spotify-local Documentation", 182 | author, 183 | "spotify-local", 184 | "One line description of project.", 185 | "Miscellaneous", 186 | ) 187 | ] 188 | 189 | 190 | # -- Options for Epub output ------------------------------------------------- 191 | 192 | # Bibliographic Dublin Core info. 193 | epub_title = project 194 | epub_author = author 195 | epub_publisher = author 196 | epub_copyright = copyright 197 | 198 | # The unique identifier of the text. This can be a ISBN number 199 | # or the project homepage. 200 | # 201 | # epub_identifier = '' 202 | 203 | # A unique identification for the text. 204 | # 205 | # epub_uid = '' 206 | 207 | # A list of files that should not be packed into the epub file. 208 | epub_exclude_files = ["search.html"] 209 | 210 | 211 | # -- Extension configuration ------------------------------------------------- 212 | 213 | # -- Options for intersphinx extension --------------------------------------- 214 | 215 | # Example configuration for intersphinx: refer to the Python standard library. 216 | intersphinx_mapping = {"https://docs.python.org/": None} 217 | 218 | # -- Options for todo extension ---------------------------------------------- 219 | 220 | # If true, `todo` and `todoList` produce output, else they produce nothing. 221 | todo_include_todos = True 222 | -------------------------------------------------------------------------------- /src/spotify_local/core.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from threading import Thread 4 | from collections import defaultdict, OrderedDict 5 | 6 | from requests import Session 7 | 8 | from .config import DEFAULT_ORIGIN 9 | from .utils import get_url, get_csrf_token, get_oauth_token 10 | 11 | 12 | class SpotifyLocal: 13 | """Controller for the local spotify web helper, throws events when the 14 | state of spotify changes. 15 | """ 16 | 17 | def __init__(self): 18 | self._registered_events = defaultdict(OrderedDict) 19 | self._csrf_token = get_csrf_token() 20 | self._oauth_token = get_oauth_token() 21 | self._session = Session() 22 | 23 | def __enter__(self): 24 | return self 25 | 26 | def __exit__(self, *args): 27 | self.close() 28 | 29 | def _request(self, url, params={}): 30 | """Makes a request using the currently open session. 31 | 32 | :param url: A url fragment to use in the creation of the master url 33 | """ 34 | r = self._session.get(url=url, params=params, headers=DEFAULT_ORIGIN) 35 | return r 36 | 37 | def close(self): 38 | self._session.close() 39 | 40 | @property 41 | def on_status_change(self): 42 | """Decorator function that adds associated callbacl to list for when a status change is detected""" 43 | 44 | def _on(func): 45 | self.add_event_handler("status_change", func) 46 | return func 47 | 48 | return _on 49 | 50 | @property 51 | def on_track_change(self): 52 | """Decorator function that adds associated callbacl to list for when a track change is detected""" 53 | 54 | def _on(func): 55 | self.add_event_handler("track_change", func) 56 | return func 57 | 58 | return _on 59 | 60 | @property 61 | def on_play_state_change(self): 62 | """Decorator function that adds associated callback to list for when a play state change is detected""" 63 | 64 | def _on(func): 65 | self.add_event_handler("play_state_change", func) 66 | return func 67 | 68 | return _on 69 | 70 | def add_event_handler(self, event, func): 71 | """Add function and event to Ordered Dict 72 | 73 | :param event: Name of the event you wish to register the function 74 | :param func: Function to register witht the associated event 75 | """ 76 | self._registered_events[event][func] = func 77 | 78 | def emit(self, event, *args, **kwargs): 79 | """Send out an event and call it's associated functions 80 | 81 | :param event: Name of the event to trigger 82 | """ 83 | for func in self._registered_events[event].values(): 84 | func(*args, **kwargs) 85 | 86 | def remove_listener(self, event, func): 87 | """Remove an event listner 88 | 89 | :param event: Event you wish to remove the function from 90 | :param func: Function you wish to remove 91 | """ 92 | self._registered_events[event].pop(func) 93 | 94 | def remove_all_listeners(self, event=None): 95 | """Remove all functions for all events, or one event if one is specifed. 96 | 97 | :param event: Optional event you wish to remove all functions from 98 | """ 99 | if event is not None: 100 | self._registered_events[event] = OrderedDict() 101 | else: 102 | self._registered_events = defaultdict(OrderedDict) 103 | 104 | def listeners(self, event): 105 | """Return list of listners associated to a particular event 106 | 107 | :param event: Name of the event you wish to query 108 | """ 109 | return list(self._registered_events[event].keys()) 110 | 111 | @property 112 | def version(self): 113 | """Spotify version information""" 114 | url: str = get_url("/service/version.json") 115 | params = {"service": "remote"} 116 | r = self._request(url=url, params=params) 117 | return r.json() 118 | 119 | def get_current_status(self): 120 | """Returns the current state of the local spotify client""" 121 | url = get_url("/remote/status.json") 122 | params = {"oauth": self._oauth_token, "csrf": self._csrf_token} 123 | r = self._request(url=url, params=params) 124 | return r.json() 125 | 126 | def pause(self, pause=True): 127 | """Pauses the spotify player 128 | 129 | :param pause: boolean value to choose the pause/play state 130 | """ 131 | url: str = get_url("/remote/pause.json") 132 | params = { 133 | "oauth": self._oauth_token, 134 | "csrf": self._csrf_token, 135 | "pause": "true" if pause else "false", 136 | } 137 | self._request(url=url, params=params) 138 | 139 | def unpause(self): 140 | """Unpauses the player by calling pause()""" 141 | self.pause(pause=False) 142 | 143 | def playURI(self, uri): 144 | """Play a Spotify uri, for example spotify:track:5Yn8WCB4Dqm8snemB5Mu4K 145 | 146 | :param uri: Playlist, Artist, Album, or Song Uri 147 | """ 148 | url: str = get_url("/remote/play.json") 149 | params = { 150 | "oauth": self._oauth_token, 151 | "csrf": self._csrf_token, 152 | "uri": uri, 153 | "context": uri, 154 | } 155 | r = self._request(url=url, params=params) 156 | return r.json() 157 | 158 | @staticmethod 159 | def skip(): 160 | """Skips the current song""" 161 | if sys.platform == "darwin": 162 | keyboard.send("KEYTYPE_SKIP") 163 | else: 164 | keyboard.send("next track") 165 | 166 | @staticmethod 167 | def previous(): 168 | """Goes to the beginning of the track, or if called twice goes to the previous track.""" 169 | if sys.platform == "darwin": 170 | keyboard.send("KEYTYPE_PREVIOUS") 171 | else: 172 | keyboard.send("previous track") 173 | 174 | def listen(self, wait=60, blocking=True): 175 | """Listen for events and call any associated callbacks when there is an event. 176 | There are three events you can subscribe too; **status_change**, **play_state_change**, **track_change** 177 | 178 | :param wait: how long to wait for a response before starting a new connection 179 | :param blocking: if listen should block the current process or not 180 | """ 181 | url = get_url("/remote/status.json") 182 | 183 | def listen_for_status_change(): 184 | params = {"oauth": self._oauth_token, "csrf": self._csrf_token} 185 | r = self._request(url=url, params=params) 186 | old = r.json() 187 | self.emit("status_change", old) 188 | params = { 189 | "oauth": self._oauth_token, 190 | "csrf": self._csrf_token, 191 | "returnon": "play,pause,error,ap", 192 | "returnafter": wait, 193 | } 194 | while True: 195 | r = self._request(url=url, params=params) 196 | new = r.json() 197 | 198 | if new != old: 199 | self.emit("status_change", new) 200 | 201 | if new["playing"] != old["playing"]: 202 | self.emit("play_state_change", new) 203 | 204 | if ( 205 | new["track"]["track_resource"]["uri"] 206 | != old["track"]["track_resource"]["uri"] 207 | ): 208 | self.emit("track_change", new) 209 | 210 | old = new 211 | 212 | if blocking: 213 | listen_for_status_change() 214 | else: 215 | thread = Thread(target=listen_for_status_change) 216 | thread.daemon = True 217 | thread.start() 218 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "6fc81f9aa47f13e5325c17941ce7792d3bc5e665beb9c5f9f291cd16ab6f3540" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.6" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "certifi": { 20 | "hashes": [ 21 | "sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7", 22 | "sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0" 23 | ], 24 | "version": "==2018.4.16" 25 | }, 26 | "chardet": { 27 | "hashes": [ 28 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 29 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 30 | ], 31 | "version": "==3.0.4" 32 | }, 33 | "delegator.py": { 34 | "hashes": [ 35 | "sha256:2d46966a7f484d271b09e2646eae1e9acadc4fdf2cb760c142f073e81c927d8d", 36 | "sha256:58f3ea6fe36680e1d828e2e66e52844b826f186409dfee4436e42351b0e699fe" 37 | ], 38 | "index": "pypi", 39 | "version": "==0.1.0" 40 | }, 41 | "idna": { 42 | "hashes": [ 43 | "sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f", 44 | "sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4" 45 | ], 46 | "version": "==2.6" 47 | }, 48 | "keyboard": { 49 | "hashes": [ 50 | "sha256:2e3df91bcea49d0a2f808af8689724ad0c28348faee4a9e0739f8e83963d2be3", 51 | "sha256:58912e4be21529fc5b57f89af7f90b0b5f5af4db08c74a8233821700eaabf189", 52 | "sha256:d4375e4be666a91b71e782fdffa017cbf1a6de98f8c9fe8b66e9386944380c45" 53 | ], 54 | "index": "pypi", 55 | "version": "==0.13.2" 56 | }, 57 | "pexpect": { 58 | "hashes": [ 59 | "sha256:2a8e88259839571d1251d278476f3eec5db26deb73a70be5ed5dc5435e418aba", 60 | "sha256:3fbd41d4caf27fa4a377bfd16fef87271099463e6fa73e92a52f92dfee5d425b" 61 | ], 62 | "version": "==4.6.0" 63 | }, 64 | "psutil": { 65 | "hashes": [ 66 | "sha256:230eeb3aeb077814f3a2cd036ddb6e0f571960d327298cc914c02385c3e02a63", 67 | "sha256:4152ae231709e3e8b80e26b6da20dc965a1a589959c48af1ed024eca6473f60d", 68 | "sha256:779ec7e7621758ca11a8d99a1064996454b3570154277cc21342a01148a49c28", 69 | "sha256:82a06785db8eeb637b349006cc28a92e40cd190fefae9875246d18d0de7ccac8", 70 | "sha256:8a15d773203a1277e57b1d11a7ccdf70804744ef4a9518a87ab8436995c31a4b", 71 | "sha256:94d4e63189f2593960e73acaaf96be235dd8a455fe2bcb37d8ad6f0e87f61556", 72 | "sha256:a3286556d4d2f341108db65d8e20d0cd3fcb9a91741cb5eb496832d7daf2a97c", 73 | "sha256:c91eee73eea00df5e62c741b380b7e5b6fdd553891bee5669817a3a38d036f13", 74 | "sha256:e2467e9312c2fa191687b89ff4bc2ad8843be4af6fb4dc95a7cc5f7d7a327b18" 75 | ], 76 | "index": "pypi", 77 | "version": "==5.4.3" 78 | }, 79 | "ptyprocess": { 80 | "hashes": [ 81 | "sha256:e64193f0047ad603b71f202332ab5527c5e52aa7c8b609704fc28c0dc20c4365", 82 | "sha256:e8c43b5eee76b2083a9badde89fd1bbce6c8942d1045146e100b7b5e014f4f1a" 83 | ], 84 | "version": "==0.5.2" 85 | }, 86 | "pyobjc-framework-quartz": { 87 | "hashes": [ 88 | "sha256:24e00ab3e50a3387d00334bc5f77d5c05825a8fbb80742420f10c4b64323ce01", 89 | "sha256:2831436fafdf6133de091a84c7b8c19c1317d51ec4655fe4d15e77a8d8d91d15", 90 | "sha256:518ea95f5aa68e8f6698dc04fc0ec0b23f181e62c994c4a2434df7b054c35906", 91 | "sha256:541a9bc32909936f49a753317576f7c8772eaadace8e462845bb19aee79531d5", 92 | "sha256:7360fdd5bb030becc317f9cb242b5e575bcec903e52764acba1a14b7e85128e3", 93 | "sha256:b2475675cd853373ac656ff86e8b3ddf2812ee2f1a44039e342d2a383aef731c", 94 | "sha256:eea4b0d7cf8bec3a8f71f736594aac03abe15ef082e63979774be4b99627cf28", 95 | "sha256:fd9e111e2c4749f77c8e60360bd73af2f271cc7631b86ca990b1066123a1c3fc" 96 | ], 97 | "index": "pypi", 98 | "markers": "sys_platform == 'dawrin'", 99 | "version": "==4.2.2" 100 | }, 101 | "requests": { 102 | "hashes": [ 103 | "sha256:6a1b267aa90cac58ac3a765d067950e7dbbf75b1da07e895d1f594193a40a38b", 104 | "sha256:9c443e7324ba5b85070c4a818ade28bfabedf16ea10206da1132edaa6dda237e" 105 | ], 106 | "index": "pypi", 107 | "version": "==2.18.4" 108 | }, 109 | "urllib3": { 110 | "hashes": [ 111 | "sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b", 112 | "sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f" 113 | ], 114 | "version": "==1.22" 115 | } 116 | }, 117 | "develop": { 118 | "aiohttp": { 119 | "hashes": [ 120 | "sha256:1a112a1fdf3802b7f2b182e22e51d71e4a8fa7387d0d38e79a268921b869e384", 121 | "sha256:33aa7c937ebaf063a860cbb0c263a771b33333a84965c6148eeafe64fb4e29ca", 122 | "sha256:550b4a0788500f6d00f41b7fdd9fcce6d78f99706a7b2f6f81d4d331c7ca468e", 123 | "sha256:601e8e83123b4d423a9dfddf7d6943f4f520651a78ffcd50c99d065136c7ff7b", 124 | "sha256:620f19ba7628b70b177f5c2e6a55a6fd6e7c8591cde38c3f8f52551733d31b66", 125 | "sha256:70d56c784da1239c89d39fefa166fd429306dada641178389be4184a9c04e501", 126 | "sha256:7de2c9e445a5d257935011268202338538abef1aaff341a4733eca56419ca6f6", 127 | "sha256:96bb80b659cc2bafa160f3f0c346ce7fc10de1ffec4908d7f9690797f155f658", 128 | "sha256:ae7501cc6a6c37b8d4774bf2218c37be47fe42019a2570e8510fc2044e59d573", 129 | "sha256:c833aa6f4c9ac3e3eb843e3d999bae51339ad33a937303f43ce78064e61cb4b6", 130 | "sha256:dd81d85a342edf3d2a388e2f24d9facebc9c04550043888f970ee2f228c93059", 131 | "sha256:f20deec7a3fbaec7b5eb7ad99878427ad2ee4cc16a46732b705e8121cbb3cc12", 132 | "sha256:f52e7287eb9286a1e91e4c67c207c2573147fbaddc68f70efb5aeee5d1992f2e", 133 | "sha256:fe7b2972ff7e779e812f974aa5695edc328ecf559ceeea887ac46f06f090ad4c", 134 | "sha256:ff1447c84a02b9cd5dd3a9332d1fb181a4386c3625765bb5caf1cfbc210ab3f9" 135 | ], 136 | "version": "==3.3.2" 137 | }, 138 | "alabaster": { 139 | "hashes": [ 140 | "sha256:674bb3bab080f598371f4443c5008cbfeb1a5e622dd312395d2d82af2c54c456", 141 | "sha256:b63b1f4dc77c074d386752ec4a8a7517600f6c0db8cd42980cae17ab7b3275d7" 142 | ], 143 | "version": "==0.7.11" 144 | }, 145 | "async-timeout": { 146 | "hashes": [ 147 | "sha256:474d4bc64cee20603e225eb1ece15e248962958b45a3648a9f5cc29e827a610c", 148 | "sha256:b3c0ddc416736619bd4a95ca31de8da6920c3b9a140c64dbef2b2fa7bf521287" 149 | ], 150 | "version": "==3.0.0" 151 | }, 152 | "attrs": { 153 | "hashes": [ 154 | "sha256:4b90b09eeeb9b88c35bc642cbac057e45a5fd85367b985bd2809c62b7b939265", 155 | "sha256:e0d0eb91441a3b53dab4d9b743eafc1ac44476296a2053b6ca3af0b139faf87b" 156 | ], 157 | "version": "==18.1.0" 158 | }, 159 | "babel": { 160 | "hashes": [ 161 | "sha256:6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669", 162 | "sha256:8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23" 163 | ], 164 | "version": "==2.6.0" 165 | }, 166 | "certifi": { 167 | "hashes": [ 168 | "sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7", 169 | "sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0" 170 | ], 171 | "version": "==2018.4.16" 172 | }, 173 | "chardet": { 174 | "hashes": [ 175 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 176 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 177 | ], 178 | "version": "==3.0.4" 179 | }, 180 | "click": { 181 | "hashes": [ 182 | "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d", 183 | "sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b" 184 | ], 185 | "version": "==6.7" 186 | }, 187 | "colorama": { 188 | "hashes": [ 189 | "sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda", 190 | "sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1" 191 | ], 192 | "markers": "sys_platform == 'win32'", 193 | "version": "==0.3.9" 194 | }, 195 | "docutils": { 196 | "hashes": [ 197 | "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", 198 | "sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", 199 | "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6" 200 | ], 201 | "version": "==0.14" 202 | }, 203 | "idna": { 204 | "hashes": [ 205 | "sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f", 206 | "sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4" 207 | ], 208 | "version": "==2.6" 209 | }, 210 | "idna-ssl": { 211 | "hashes": [ 212 | "sha256:1293f030bc608e9aa9cdee72aa93c1521bbb9c7698068c61c9ada6772162b979" 213 | ], 214 | "version": "==1.0.1" 215 | }, 216 | "imagesize": { 217 | "hashes": [ 218 | "sha256:3620cc0cadba3f7475f9940d22431fc4d407269f1be59ec9b8edcca26440cf18", 219 | "sha256:5b326e4678b6925158ccc66a9fa3122b6106d7c876ee32d7de6ce59385b96315" 220 | ], 221 | "version": "==1.0.0" 222 | }, 223 | "incremental": { 224 | "hashes": [ 225 | "sha256:717e12246dddf231a349175f48d74d93e2897244939173b01974ab6661406b9f", 226 | "sha256:7b751696aaf36eebfab537e458929e194460051ccad279c72b755a167eebd4b3" 227 | ], 228 | "version": "==17.5.0" 229 | }, 230 | "jinja2": { 231 | "hashes": [ 232 | "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", 233 | "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" 234 | ], 235 | "version": "==2.10" 236 | }, 237 | "keyboard": { 238 | "hashes": [ 239 | "sha256:2e3df91bcea49d0a2f808af8689724ad0c28348faee4a9e0739f8e83963d2be3", 240 | "sha256:58912e4be21529fc5b57f89af7f90b0b5f5af4db08c74a8233821700eaabf189", 241 | "sha256:d4375e4be666a91b71e782fdffa017cbf1a6de98f8c9fe8b66e9386944380c45" 242 | ], 243 | "index": "pypi", 244 | "version": "==0.13.2" 245 | }, 246 | "markupsafe": { 247 | "hashes": [ 248 | "sha256:a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665" 249 | ], 250 | "version": "==1.0" 251 | }, 252 | "multidict": { 253 | "hashes": [ 254 | "sha256:1a1d76374a1e7fe93acef96b354a03c1d7f83e7512e225a527d283da0d7ba5e0", 255 | "sha256:1d6e191965505652f194bc4c40270a842922685918a4f45e6936a6b15cc5816d", 256 | "sha256:295961a6a88f1199e19968e15d9b42f3a191c89ec13034dbc212bf9c394c3c82", 257 | "sha256:2be5af084de6c3b8e20d6421cb0346378a9c867dcf7c86030d6b0b550f9888e4", 258 | "sha256:2eb99617c7a0e9f2b90b64bc1fb742611718618572747d6f3d6532b7b78755ab", 259 | "sha256:4ba654c6b5ad1ae4a4d792abeb695b29ce981bb0f157a41d0fd227b385f2bef0", 260 | "sha256:5ba766433c30d703f6b2c17eb0b6826c6f898e5f58d89373e235f07764952314", 261 | "sha256:a59d58ee85b11f337b54933e8d758b2356fcdcc493248e004c9c5e5d11eedbe4", 262 | "sha256:a6e35d28900cf87bcc11e6ca9e474db0099b78f0be0a41d95bef02d49101b5b2", 263 | "sha256:b4df7ca9c01018a51e43937eaa41f2f5dce17a6382fda0086403bcb1f5c2cf8e", 264 | "sha256:bbd5a6bffd3ba8bfe75b16b5e28af15265538e8be011b0b9fddc7d86a453fd4a", 265 | "sha256:d870f399fcd58a1889e93008762a3b9a27cf7ea512818fc6e689f59495648355", 266 | "sha256:e9404e2e19e901121c3c5c6cffd5a8ae0d1d67919c970e3b3262231175713068" 267 | ], 268 | "version": "==4.3.1" 269 | }, 270 | "mypy": { 271 | "hashes": [ 272 | "sha256:1b899802a89b67bb68f30d788bba49b61b1f28779436f06b75c03495f9d6ea5c", 273 | "sha256:f472645347430282d62d1f97d12ccb8741f19f1572b7cf30b58280e4e0818739" 274 | ], 275 | "index": "pypi", 276 | "version": "==0.610" 277 | }, 278 | "packaging": { 279 | "hashes": [ 280 | "sha256:e9215d2d2535d3ae866c3d6efc77d5b24a0192cce0ff20e42896cc0664f889c0", 281 | "sha256:f019b770dd64e585a99714f1fd5e01c7a8f11b45635aa953fd41c689a657375b" 282 | ], 283 | "version": "==17.1" 284 | }, 285 | "pkginfo": { 286 | "hashes": [ 287 | "sha256:5878d542a4b3f237e359926384f1dde4e099c9f5525d236b1840cf704fa8d474", 288 | "sha256:a39076cb3eb34c333a0dd390b568e9e1e881c7bf2cc0aee12120636816f55aee" 289 | ], 290 | "version": "==1.4.2" 291 | }, 292 | "pygments": { 293 | "hashes": [ 294 | "sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d", 295 | "sha256:dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc" 296 | ], 297 | "version": "==2.2.0" 298 | }, 299 | "pyparsing": { 300 | "hashes": [ 301 | "sha256:0832bcf47acd283788593e7a0f542407bd9550a55a8a8435214a1960e04bcb04", 302 | "sha256:281683241b25fe9b80ec9d66017485f6deff1af5cde372469134b56ca8447a07", 303 | "sha256:8f1e18d3fd36c6795bb7e02a39fd05c611ffc2596c1e0d995d34d67630426c18", 304 | "sha256:9e8143a3e15c13713506886badd96ca4b579a87fbdf49e550dbfc057d6cb218e", 305 | "sha256:b8b3117ed9bdf45e14dcc89345ce638ec7e0e29b2b579fa1ecf32ce45ebac8a5", 306 | "sha256:e4d45427c6e20a59bf4f88c639dcc03ce30d193112047f94012102f235853a58", 307 | "sha256:fee43f17a9c4087e7ed1605bd6df994c6173c1e977d7ade7b651292fab2bd010" 308 | ], 309 | "version": "==2.2.0" 310 | }, 311 | "pytz": { 312 | "hashes": [ 313 | "sha256:65ae0c8101309c45772196b21b74c46b2e5d11b6275c45d251b150d5da334555", 314 | "sha256:c06425302f2cf668f1bba7a0a03f3c1d34d4ebeef2c72003da308b3947c7f749" 315 | ], 316 | "version": "==2018.4" 317 | }, 318 | "requests": { 319 | "hashes": [ 320 | "sha256:6a1b267aa90cac58ac3a765d067950e7dbbf75b1da07e895d1f594193a40a38b", 321 | "sha256:9c443e7324ba5b85070c4a818ade28bfabedf16ea10206da1132edaa6dda237e" 322 | ], 323 | "index": "pypi", 324 | "version": "==2.18.4" 325 | }, 326 | "requests-toolbelt": { 327 | "hashes": [ 328 | "sha256:42c9c170abc2cacb78b8ab23ac957945c7716249206f90874651971a4acff237", 329 | "sha256:f6a531936c6fa4c6cfce1b9c10d5c4f498d16528d2a54a22ca00011205a187b5" 330 | ], 331 | "version": "==0.8.0" 332 | }, 333 | "rope": { 334 | "hashes": [ 335 | "sha256:a09edfd2034fd50099a67822f9bd851fbd0f4e98d3b87519f6267b60e50d80d1" 336 | ], 337 | "index": "pypi", 338 | "version": "==0.10.7" 339 | }, 340 | "six": { 341 | "hashes": [ 342 | "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", 343 | "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" 344 | ], 345 | "version": "==1.11.0" 346 | }, 347 | "snowballstemmer": { 348 | "hashes": [ 349 | "sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128", 350 | "sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89" 351 | ], 352 | "version": "==1.2.1" 353 | }, 354 | "sphinx": { 355 | "hashes": [ 356 | "sha256:85f7e32c8ef07f4ba5aeca728e0f7717bef0789fba8458b8d9c5c294cad134f3", 357 | "sha256:d45480a229edf70d84ca9fae3784162b1bc75ee47e480ffe04a4b7f21a95d76d" 358 | ], 359 | "index": "pypi", 360 | "version": "==1.7.5" 361 | }, 362 | "sphinxcontrib-websupport": { 363 | "hashes": [ 364 | "sha256:68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd", 365 | "sha256:9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9" 366 | ], 367 | "version": "==1.1.0" 368 | }, 369 | "spotify-local": { 370 | "editable": true, 371 | "path": "R:/spotipy-local" 372 | }, 373 | "toml": { 374 | "hashes": [ 375 | "sha256:8e86bd6ce8cc11b9620cb637466453d94f5d57ad86f17e98a98d1f73e3baab2d" 376 | ], 377 | "version": "==0.9.4" 378 | }, 379 | "towncrier": { 380 | "hashes": [ 381 | "sha256:2b6da2c90c0f053232775d96e816687bf607062b9b2947f625f5586c357cbd59", 382 | "sha256:4643dafdde7d503cd6990dfba8043faa9f920fc4af0c2e69eb25488760534ac0" 383 | ], 384 | "index": "pypi", 385 | "version": "==18.5.0" 386 | }, 387 | "tqdm": { 388 | "hashes": [ 389 | "sha256:224291ee0d8c52d91b037fd90806f48c79bcd9994d3b0abc9e44b946a908fccd", 390 | "sha256:77b8424d41b31e68f437c6dd9cd567aebc9a860507cb42fbd880a5f822d966fe" 391 | ], 392 | "version": "==4.23.4" 393 | }, 394 | "twine": { 395 | "hashes": [ 396 | "sha256:08eb132bbaec40c6d25b358f546ec1dc96ebd2638a86eea68769d9e67fe2b129", 397 | "sha256:2fd9a4d9ff0bcacf41fdc40c8cb0cfaef1f1859457c9653fd1b92237cc4e9f25" 398 | ], 399 | "index": "pypi", 400 | "version": "==1.11.0" 401 | }, 402 | "typed-ast": { 403 | "hashes": [ 404 | "sha256:0948004fa228ae071054f5208840a1e88747a357ec1101c17217bfe99b299d58", 405 | "sha256:25d8feefe27eb0303b73545416b13d108c6067b846b543738a25ff304824ed9a", 406 | "sha256:29464a177d56e4e055b5f7b629935af7f49c196be47528cc94e0a7bf83fbc2b9", 407 | "sha256:2e214b72168ea0275efd6c884b114ab42e316de3ffa125b267e732ed2abda892", 408 | "sha256:3e0d5e48e3a23e9a4d1a9f698e32a542a4a288c871d33ed8df1b092a40f3a0f9", 409 | "sha256:519425deca5c2b2bdac49f77b2c5625781abbaf9a809d727d3a5596b30bb4ded", 410 | "sha256:57fe287f0cdd9ceaf69e7b71a2e94a24b5d268b35df251a88fef5cc241bf73aa", 411 | "sha256:668d0cec391d9aed1c6a388b0d5b97cd22e6073eaa5fbaa6d2946603b4871efe", 412 | "sha256:68ba70684990f59497680ff90d18e756a47bf4863c604098f10de9716b2c0bdd", 413 | "sha256:6de012d2b166fe7a4cdf505eee3aaa12192f7ba365beeefaca4ec10e31241a85", 414 | "sha256:79b91ebe5a28d349b6d0d323023350133e927b4de5b651a8aa2db69c761420c6", 415 | "sha256:8550177fa5d4c1f09b5e5f524411c44633c80ec69b24e0e98906dd761941ca46", 416 | "sha256:a8034021801bc0440f2e027c354b4eafd95891b573e12ff0418dec385c76785c", 417 | "sha256:bc978ac17468fe868ee589c795d06777f75496b1ed576d308002c8a5756fb9ea", 418 | "sha256:c05b41bc1deade9f90ddc5d988fe506208019ebba9f2578c622516fd201f5863", 419 | "sha256:c9b060bd1e5a26ab6e8267fd46fc9e02b54eb15fffb16d112d4c7b1c12987559", 420 | "sha256:edb04bdd45bfd76c8292c4d9654568efaedf76fe78eb246dde69bdb13b2dad87", 421 | "sha256:f19f2a4f547505fe9072e15f6f4ae714af51b5a681a97f187971f50c283193b6" 422 | ], 423 | "version": "==1.1.0" 424 | }, 425 | "urllib3": { 426 | "hashes": [ 427 | "sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b", 428 | "sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f" 429 | ], 430 | "version": "==1.22" 431 | }, 432 | "white": { 433 | "hashes": [ 434 | "sha256:45e2c7f54de1facc60bf0a726b480cdc43422aad57c3a0bc5ba54cb536696683", 435 | "sha256:bca98066256cfff6fb85ec36b95cc5913c888c170a8407c340786972b06c6f8f" 436 | ], 437 | "index": "pypi", 438 | "version": "==0.1.2" 439 | }, 440 | "yarl": { 441 | "hashes": [ 442 | "sha256:2556b779125621b311844a072e0ed367e8409a18fa12cbd68eb1258d187820f9", 443 | "sha256:4aec0769f1799a9d4496827292c02a7b1f75c0bab56ab2b60dd94ebb57cbd5ee", 444 | "sha256:55369d95afaacf2fa6b49c84d18b51f1704a6560c432a0f9a1aeb23f7b971308", 445 | "sha256:6c098b85442c8fe3303e708bbb775afd0f6b29f77612e8892627bcab4b939357", 446 | "sha256:9182cd6f93412d32e009020a44d6d170d2093646464a88aeec2aef50592f8c78", 447 | "sha256:c8cbc21bbfa1dd7d5386d48cc814fe3d35b80f60299cdde9279046f399c3b0d8", 448 | "sha256:db6f70a4b09cde813a4807843abaaa60f3b15fb4a2a06f9ae9c311472662daa1", 449 | "sha256:f17495e6fe3d377e3faac68121caef6f974fcb9e046bc075bcff40d8e5cc69a4", 450 | "sha256:f85900b9cca0c67767bb61b2b9bd53208aaa7373dae633dbe25d179b4bf38aa7" 451 | ], 452 | "version": "==1.2.6" 453 | } 454 | } 455 | } 456 | --------------------------------------------------------------------------------