├── 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 |
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 |
2 |
3 |
4 |
5 |
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 |
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 |
--------------------------------------------------------------------------------