├── spotrpy
├── __init__.py
├── api
│ ├── __init__.py
│ └── API.py
├── assets
│ ├── __init__.py
│ └── LOGO.py
├── config
│ ├── __init__.py
│ └── config.py
├── core
│ ├── __init__.py
│ └── BaseController.py
├── logging
│ ├── __init__.py
│ └── Logging.py
├── parser
│ ├── __init__.py
│ └── parser.py
├── util
│ ├── __init__.py
│ ├── Helpers.py
│ └── ASCII.py
├── controllers
│ ├── __init__.py
│ ├── authController.py
│ ├── previousController.py
│ ├── nextController.py
│ ├── startController.py
│ ├── stopController.py
│ ├── replayController.py
│ ├── seekController.py
│ ├── webController.py
│ ├── queueController.py
│ ├── qsearchController.py
│ ├── shuffleController.py
│ ├── playbackController.py
│ ├── volumeController.py
│ ├── asciiController.py
│ ├── playlistController.py
│ ├── recentController.py
│ ├── playlistaddController.py
│ ├── recommendController.py
│ ├── searchController.py
│ ├── artistController.py
│ └── currentController.py
├── interface
│ ├── __init__.py
│ └── Command.py
├── main.py
├── commands.py
└── .pylintrc
├── .gitignore
├── requirements.txt
├── setup.py
├── LICENSE.md
└── README.md
/spotrpy/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spotrpy/api/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spotrpy/assets/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spotrpy/config/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spotrpy/core/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spotrpy/logging/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spotrpy/parser/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spotrpy/util/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spotrpy/controllers/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spotrpy/interface/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.egg-info
2 | build/*
3 | dist/*
4 | **/__pycache__
5 | **/config.json
6 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Pillow==10.1.0
2 | questionary==2.0.1
3 | requests==2.28.1
4 | rich==13.6.0
5 | tqdm==4.66.1
--------------------------------------------------------------------------------
/spotrpy/main.py:
--------------------------------------------------------------------------------
1 | from spotrpy.parser.parser import Parser
2 |
3 | def main():
4 | Parser()
5 |
6 | if __name__ == "__main__":
7 | main()
8 |
--------------------------------------------------------------------------------
/spotrpy/controllers/authController.py:
--------------------------------------------------------------------------------
1 | from spotrpy.core.BaseController import BaseController
2 |
3 |
4 | class authController(BaseController):
5 |
6 | def auth(self):
7 | self.authorise()
8 |
--------------------------------------------------------------------------------
/spotrpy/controllers/previousController.py:
--------------------------------------------------------------------------------
1 | from urllib.parse import urljoin
2 |
3 | from spotrpy.core.BaseController import BaseController
4 |
5 | class previousController(BaseController):
6 |
7 | def run(self):
8 | self.request("POST", urljoin(self.API_PLAYER, "previous"))
9 |
--------------------------------------------------------------------------------
/spotrpy/controllers/nextController.py:
--------------------------------------------------------------------------------
1 | from urllib.parse import urljoin
2 |
3 | from spotrpy.core.BaseController import BaseController
4 |
5 |
6 | class nextController(BaseController):
7 |
8 | def run(self):
9 | self.request("POST", urljoin(self.API_PLAYER, "next"))
10 |
--------------------------------------------------------------------------------
/spotrpy/controllers/startController.py:
--------------------------------------------------------------------------------
1 |
2 | from urllib.parse import urljoin
3 |
4 | from spotrpy.core.BaseController import BaseController
5 |
6 |
7 | class startController(BaseController):
8 |
9 | def run(self):
10 | self.request("PUT", urljoin(self.API_PLAYER, "play"))
11 |
--------------------------------------------------------------------------------
/spotrpy/controllers/stopController.py:
--------------------------------------------------------------------------------
1 |
2 | from urllib.parse import urljoin
3 |
4 | from spotrpy.core.BaseController import BaseController
5 |
6 |
7 | class stopController(BaseController):
8 |
9 | def run(self):
10 | self.request("PUT", urljoin(self.API_PLAYER, "pause"))
11 |
--------------------------------------------------------------------------------
/spotrpy/controllers/replayController.py:
--------------------------------------------------------------------------------
1 |
2 | from urllib.parse import urljoin, urlencode
3 |
4 | from spotrpy.core.BaseController import BaseController
5 |
6 |
7 | class replayController(BaseController):
8 |
9 | def run(self):
10 | self.request("PUT", f"{urljoin(self.API_PLAYER, 'seek')}?{urlencode({'position_ms': 0})}")
11 |
--------------------------------------------------------------------------------
/spotrpy/assets/LOGO.py:
--------------------------------------------------------------------------------
1 | SPOTR_LOGO = r"""
2 | ______ ______ ______ ______ ______
3 | /\ ___\ /\ == \ /\ __ \ /\__ _\ /\ == \
4 | \ \___ \ \ \ _-/ \ \ \/\ \ \/_/\ \/ \ \ __<
5 | \/\_____\ \ \_\ \ \_____\ \ \_\ \ \_\ \_\
6 | \/_____/ \/_/ \/_____/ \/_/ \/_/ /_/
7 | by Havard03
8 | """
9 |
--------------------------------------------------------------------------------
/spotrpy/controllers/seekController.py:
--------------------------------------------------------------------------------
1 |
2 | from urllib.parse import urljoin, urlencode
3 |
4 | from spotrpy.core.BaseController import BaseController
5 |
6 |
7 | class seekController(BaseController):
8 |
9 | def run(self):
10 | self.request(
11 | "PUT",
12 | str(f"{urljoin(self.API_PLAYER, 'seek')}?{urlencode({'position_ms': int(self.args.seconds) * 1000})}")
13 | )
14 |
--------------------------------------------------------------------------------
/spotrpy/controllers/webController.py:
--------------------------------------------------------------------------------
1 | import webbrowser
2 | from urllib.parse import urljoin
3 |
4 | from spotrpy.core.BaseController import BaseController
5 |
6 | class webController(BaseController):
7 |
8 | def run(self):
9 | data = self.request("GET", urljoin(self.API_PLAYER, "currently-playing"))
10 | if data is None: return
11 | webbrowser.open_new_tab(data["item"]["external_urls"]["spotify"])
12 |
--------------------------------------------------------------------------------
/spotrpy/controllers/queueController.py:
--------------------------------------------------------------------------------
1 | from urllib.parse import urljoin
2 |
3 | from spotrpy.core.BaseController import BaseController
4 |
5 |
6 | class queueController(BaseController):
7 |
8 | def run(self):
9 | self.fetch()
10 | for track in self.response["queue"]:
11 | self.console.print(f"{track['name']}")
12 |
13 | def fetch(self):
14 | self.response = self.request("GET", urljoin(self.API_PLAYER, "queue"))
15 |
--------------------------------------------------------------------------------
/spotrpy/interface/Command.py:
--------------------------------------------------------------------------------
1 | """
2 | Command interface
3 | """
4 |
5 | from typing import Any, Callable, List, Union
6 |
7 |
8 | class Command():
9 | """ Command """
10 |
11 | commands = {}
12 |
13 | def command(self, names: List[str], desc: str, controller: Union[Callable[..., Any], List[Any]], options=None) -> None:
14 | for name in names:
15 | self.commands[name]=({
16 | "name": name,
17 | "desc": desc,
18 | "callable": controller or [],
19 | "options": options or []
20 | })
21 |
--------------------------------------------------------------------------------
/spotrpy/core/BaseController.py:
--------------------------------------------------------------------------------
1 | from spotrpy.logging.Logging import Logging
2 | from spotrpy.config.config import Config
3 | from spotrpy.api.API import API
4 |
5 | """
6 | BaseController for bootstrapping essential functionality
7 | """
8 |
9 | class BaseController(Logging, Config, API):
10 | """ BaseController """
11 |
12 | def __init__(self, args):
13 | self.args = args
14 |
15 | self.__initLogging__()
16 | self.log.debug("[green]BaseController initialized [purple]Logging")
17 |
18 | self.__initConfig__()
19 | self.log.debug("[green]BaseController initialized [purple]Config")
20 |
21 | self.__initAPI__()
22 | self.log.debug("[green]BaseController initialized [purple]API")
23 |
--------------------------------------------------------------------------------
/spotrpy/controllers/qsearchController.py:
--------------------------------------------------------------------------------
1 |
2 | from urllib.parse import urljoin, urlencode
3 |
4 | from spotrpy.core.BaseController import BaseController
5 |
6 |
7 | class qsearchController(BaseController):
8 |
9 | def run(self):
10 | self.fetch()
11 | json_id = [self.response['tracks']['items'][0]['uri']]
12 | json = {"uris": json_id, "offset": {"position": "0"}}
13 |
14 | self.request("PUT", str(urljoin(self.API_PLAYER, "play")), json=json)
15 |
16 | def fetch(self):
17 | self.response = self.request(
18 | "GET",
19 | str(
20 | f"{urljoin(self.API_BASE_VERSION, 'search')}?{urlencode({'q': ' '.join(self.args.query), 'type': 'track', 'limit': 1})}"
21 | ),
22 | )
23 |
--------------------------------------------------------------------------------
/spotrpy/controllers/shuffleController.py:
--------------------------------------------------------------------------------
1 | import questionary
2 | from urllib.parse import urljoin, urlencode
3 |
4 | from spotrpy.core.BaseController import BaseController
5 |
6 |
7 | class shuffleController(BaseController):
8 |
9 | def run(self):
10 | if not self.args.state:
11 | state = questionary.select(
12 | "Choose shuffle state",
13 | choices=["true", "false"],
14 | erase_when_done=True,
15 | use_shortcuts=True,
16 | ).ask()
17 | else:
18 | state = self.args.state
19 |
20 | if state is None:
21 | return
22 |
23 | self.request("PUT", str(f"{urljoin(self.API_PLAYER, 'shuffle')}?{urlencode({'state': state})}"))
24 |
--------------------------------------------------------------------------------
/spotrpy/controllers/playbackController.py:
--------------------------------------------------------------------------------
1 | import questionary
2 | from urllib.parse import urljoin, urlencode
3 |
4 | from spotrpy.core.BaseController import BaseController
5 |
6 |
7 | class playbackController(BaseController):
8 |
9 | def run(self):
10 | if not self.args.state:
11 | state = questionary.select(
12 | "Choose a play state",
13 | choices=["track", "context", "off"],
14 | erase_when_done=True,
15 | use_shortcuts=True,
16 | ).ask()
17 | else:
18 | state = self.args.state
19 |
20 | if state is None:
21 | return
22 |
23 | self.request("PUT", str(f"{urljoin(self.API_PLAYER, 'repeat')}?{urlencode({'state': state})}"))
24 |
25 |
--------------------------------------------------------------------------------
/spotrpy/controllers/volumeController.py:
--------------------------------------------------------------------------------
1 | import questionary
2 | from urllib.parse import urljoin, urlencode
3 |
4 | from spotrpy.core.BaseController import BaseController
5 |
6 | class volumeController(BaseController):
7 |
8 | def run(self):
9 | if self.args.percentage is None:
10 | volume = questionary.select(
11 | "Choose volume precentage",
12 | choices=["25%", "50%", "75%", "100%"],
13 | erase_when_done=True,
14 | use_shortcuts=True,
15 | ).ask()
16 | else:
17 | volume = self.args.percentage
18 |
19 | self.request(
20 | "PUT",
21 | str(
22 | f"{urljoin(self.API_PLAYER, 'volume')}?{urlencode({'volume_percent': str(volume).replace('%', '')})}"
23 | ),
24 | )
25 |
--------------------------------------------------------------------------------
/spotrpy/logging/Logging.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from rich.console import Console
4 | from rich.logging import RichHandler
5 |
6 | """
7 | Universal logging config
8 | """
9 |
10 | class Logging:
11 | """ Logging """
12 |
13 | def __initLogging__(self):
14 | self.log = logging.getLogger()
15 | self.console = Console()
16 |
17 | if self.args.debug:
18 | logging.basicConfig(
19 | level="NOTSET",
20 | format="%(message)s",
21 | datefmt="[%X]",
22 | handlers=[RichHandler(markup=True, rich_tracebacks=True)],
23 | )
24 | else:
25 | logging.basicConfig(
26 | level="INFO",
27 | datefmt="[%X]",
28 | format="%(message)s",
29 | handlers=[RichHandler(markup=True, rich_tracebacks=True)],
30 | )
31 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | """Setup script for spotrpy package"""
2 |
3 | from setuptools import setup, find_packages
4 |
5 | with open("README.md", "r", encoding = "utf-8") as fh:
6 | long_description = fh.read()
7 |
8 | setup(
9 | name='spotrpy',
10 | version='4.1',
11 | packages=find_packages(),
12 | python_requires = ">=3.6",
13 | author = "Havard03",
14 | author_email = "havard.buvang@gmail.com",
15 | description = "A simple spotify tool for the terminal",
16 | long_description = long_description,
17 | long_description_content_type = "text/markdown",
18 | url = "https://github.com/Havard03/spotr",
19 | keywords = 'cli',
20 | install_requires=[
21 | 'Pillow',
22 | 'questionary',
23 | 'requests',
24 | 'rich',
25 | 'tqdm',
26 | ],
27 | entry_points={
28 | 'console_scripts': [
29 | 'spotr=spotrpy.main:main',
30 | ],
31 | },
32 | classifiers=[
33 | 'Programming Language :: Python :: 3',
34 | 'License :: OSI Approved :: MIT License',
35 | 'Operating System :: OS Independent',
36 | ],
37 | )
38 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Havard03
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 |
--------------------------------------------------------------------------------
/spotrpy/controllers/asciiController.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | from urllib.parse import urljoin
4 |
5 | from spotrpy.core.BaseController import BaseController
6 | from spotrpy.util.ASCII import ASCII
7 |
8 |
9 | class asciiController(BaseController, ASCII):
10 |
11 | def run(self):
12 | self.fetch()
13 | if self.args.width is None:
14 | width, height = os.get_terminal_size()
15 | else:
16 | width = self.args.width
17 |
18 | ascii_str = (
19 | self.image_to_ascii_color(
20 | self.response["item"]["album"]["images"][0]["url"], int(width)
21 | )
22 | if eval(self.CONFIG["ASCII_IMAGE_COLOR"])
23 | else self.image_to_ascii(
24 | self.response["item"]["album"]["images"][0]["url"], int(width)
25 | )
26 | )
27 | lines = ascii_str.splitlines()
28 | for line in lines:
29 | print(line)
30 |
31 | def fetch(self):
32 | self.response = self.request("GET", urljoin(self.API_PLAYER, "currently-playing"))
33 | if self.response is None or self.response["item"] is None:
34 | self.log.error("No data")
35 | sys.exit()
36 |
--------------------------------------------------------------------------------
/spotrpy/controllers/playlistController.py:
--------------------------------------------------------------------------------
1 | import questionary
2 |
3 | from urllib.parse import urljoin, urlencode
4 |
5 | from spotrpy.core.BaseController import BaseController
6 | from spotrpy.util.Helpers import Helpers
7 |
8 |
9 | class playlistController(BaseController, Helpers):
10 |
11 | def run(self):
12 | self.fetch()
13 |
14 | choices = self.parse_items(
15 | self.response,
16 | accessor=["items"],
17 | return_value=["uri"],
18 | name_value=["name"],
19 | artists_value=False,
20 | )
21 |
22 | selected = questionary.select(
23 | "What playlist do you want to play?",
24 | choices=choices,
25 | erase_when_done=True,
26 | use_arrow_keys=True,
27 | use_jk_keys=False,
28 | ).ask()
29 | if selected is None:
30 | return
31 |
32 | json = {"context_uri": selected, "offset": {"position": "0"}}
33 | self.request("PUT", str(urljoin(self.API_PLAYER, "play")), json=json)
34 |
35 | def fetch(self):
36 | self.response = self.request("GET", f"{urljoin(self.API_BASE_VERSION, 'me/playlists')}?{urlencode({'limit': self.SPOTIFY_LIMIT})}")
37 |
--------------------------------------------------------------------------------
/spotrpy/controllers/recentController.py:
--------------------------------------------------------------------------------
1 | import questionary
2 | from urllib.parse import urljoin, urlencode
3 |
4 | from spotrpy.core.BaseController import BaseController
5 | from spotrpy.util.Helpers import Helpers
6 |
7 |
8 | class recentController(BaseController, Helpers):
9 |
10 | def run(self):
11 | self.fetch()
12 |
13 | choices = self.parse_items(
14 | self.response,
15 | accessor=["items"],
16 | return_value=["track", "uri"],
17 | name_value=["track", "name"],
18 | artists_value=["track", "artists"],
19 | )
20 |
21 | selected = questionary.select(
22 | "What song do you want to play?",
23 | choices=choices,
24 | erase_when_done=True,
25 | use_shortcuts=True,
26 | use_arrow_keys=True,
27 | use_jk_keys=False,
28 | ).ask()
29 |
30 | if selected is None:
31 | return
32 |
33 | json = {"uris": [selected]}
34 | self.request("PUT", str(urljoin(self.API_PLAYER, "play")), json=json)
35 |
36 | def fetch(self):
37 | self.response = self.request("GET", f"{urljoin(self.API_PLAYER, 'recently-played')}?{urlencode({'limit' : self.QUSTIONARY_LIMIT})}")
38 |
--------------------------------------------------------------------------------
/spotrpy/controllers/playlistaddController.py:
--------------------------------------------------------------------------------
1 | import questionary
2 | from urllib.parse import urljoin, urlencode
3 |
4 | from spotrpy.core.BaseController import BaseController
5 | from spotrpy.util.Helpers import Helpers
6 |
7 |
8 | class playlistaddController(BaseController, Helpers):
9 |
10 | def run(self):
11 | self.fetch()
12 | choices = self.parse_items(
13 | self.response,
14 | accessor=["items"],
15 | return_value=["id"],
16 | name_value=["name"],
17 | artists_value=False,
18 | )
19 |
20 | selected = questionary.select(
21 | "what playlist do you want to add track to?",
22 | choices=choices,
23 | erase_when_done=True,
24 | use_arrow_keys=True,
25 | use_jk_keys=False,
26 | ).ask()
27 |
28 | if selected is None:
29 | return
30 |
31 | self.request(
32 | "POST",
33 | str(f'{urljoin(self.API_BASE_VERSION, f"playlists/{selected}/tracks")}?{urlencode({"uris": self.current_song["item"]["uri"]})}')
34 | )
35 |
36 | def fetch(self):
37 | self.response = self.request("GET", f"{urljoin(self.API_BASE_VERSION, 'me/playlists')}?{urlencode({'limit': self.SPOTIFY_LIMIT})}")
38 | self.current_song = self.request("GET", urljoin(self.API_PLAYER, "currently-playing"))
39 |
40 |
--------------------------------------------------------------------------------
/spotrpy/controllers/recommendController.py:
--------------------------------------------------------------------------------
1 | from urllib.parse import urljoin, urlencode
2 |
3 | from spotrpy.core.BaseController import BaseController
4 |
5 |
6 | class recommendController(BaseController):
7 |
8 | def run(self):
9 | self.fetch()
10 |
11 | seed_arists = []
12 | seed_generes = ["all"]
13 | seed_tracks = []
14 |
15 | for track in self.response["items"]:
16 | seed_tracks.append(track["track"]["id"])
17 | seed_arists.append(track["track"]["artists"][0]["id"])
18 |
19 | params = urlencode(
20 | {
21 | 'seed_arists': ','.join(seed_arists),
22 | 'seed_generes': ','.join(seed_generes),
23 | 'seed_tracks': ','.join(seed_tracks),
24 | 'limit': 5,
25 | }
26 | )
27 |
28 | recommended = self.request(
29 | "GET",
30 | str(
31 | f"{urljoin(self.API_BASE_VERSION, 'recommendations')}?{params}"
32 | ),
33 | )
34 | results = []
35 | for track in recommended["tracks"]:
36 | results.append(track["uri"])
37 | json = {"uris": results, "offset": {"position": "0"}}
38 |
39 | self.request("PUT", str(urljoin(self.API_PLAYER, "play")), json=json)
40 |
41 | def fetch(self):
42 | self.response = self.request(
43 | "GET", f"{urljoin(self.API_PLAYER, 'recently-played')}?{urlencode({'limit': 5})}"
44 | )
45 |
--------------------------------------------------------------------------------
/spotrpy/util/Helpers.py:
--------------------------------------------------------------------------------
1 | class Helpers():
2 | """ Helpers """
3 |
4 | def __get_item(self, data, keys):
5 | """ Access data with accessor """
6 |
7 | for key in keys:
8 | data = data[key]
9 | return data
10 |
11 | def parse_items(
12 | self,
13 | data,
14 | name_value,
15 | accessor=None,
16 | return_value=None,
17 | artists_value=False,
18 | artists_array=True,
19 | ):
20 | """ Parse tracks for questionary """
21 |
22 | choices = []
23 | track_width = 0
24 |
25 | if artists_value:
26 | names = [
27 | self.__get_item(item, name_value)
28 | for item in (self.__get_item(data, accessor) if accessor else data)
29 | ]
30 | if names:
31 | track_width = max(map(len, names))
32 |
33 | for item in (self.__get_item(data, accessor) if accessor else data):
34 | track_name = self.__get_item(item, name_value)
35 | if artists_value:
36 | if artists_array:
37 | artist_names = ", ".join(
38 | artist["name"]
39 | for artist in (self.__get_item(item, artists_value))
40 | )
41 | else:
42 | artist_names = self.__get_item(item, artists_value)
43 | choices.append(
44 | {
45 | "name": "{0:<{track_width}} -- {1}".format(
46 | track_name, artist_names, track_width=track_width
47 | ),
48 | "value": self.__get_item(item, return_value) if return_value else item,
49 | }
50 | )
51 | else:
52 | choices.append(
53 | {
54 | "name": track_name,
55 | "value": self.__get_item(item, return_value) if return_value else item,
56 | }
57 | )
58 |
59 | return choices
60 |
61 |
62 |
--------------------------------------------------------------------------------
/spotrpy/util/ASCII.py:
--------------------------------------------------------------------------------
1 | import requests
2 |
3 | from io import BytesIO
4 | from PIL import Image
5 |
6 | class ASCII:
7 | """ ASCII class """
8 |
9 | def resize_image(self, image, new_width=100):
10 | """ Rezise image """
11 |
12 | width, height = image.size
13 | ratio = height / width / 2.25
14 | new_height = int(new_width * ratio)
15 | resized_image = image.resize((new_width, new_height))
16 |
17 | return resized_image
18 |
19 | def image_to_ascii(self, image_url, desired_width=75):
20 | """ Convert image to ascii """
21 |
22 | response = requests.get(image_url, timeout=10)
23 | image_data = response.content
24 | image = Image.open(BytesIO(image_data))
25 | image = self.resize_image(image, desired_width)
26 | image = image.convert("L")
27 | pixels = image.getdata()
28 | ascii_str = ""
29 | ascii_art = ""
30 |
31 | for pixel_value in pixels:
32 | ascii_str += self.CONFIG["ASCII_IMAGE_CHARS"][
33 | max(
34 | 0, min(pixel_value // 16, len(self.CONFIG["ASCII_IMAGE_CHARS"]) - 1)
35 | )
36 | ]
37 |
38 | for i in range(0, len(ascii_str), int(desired_width)):
39 | row = ascii_str[i : i + int(desired_width)]
40 | ascii_art += row + "\n"
41 |
42 | return ascii_art
43 |
44 | def rgb_to_ansi(self, r, g, b):
45 | """ Convert RGB color values to ANSI color codes for console """
46 |
47 | return f"\x1b[38;2;{r};{g};{b}m"
48 |
49 | def image_to_ascii_color(self, image_url, width=75):
50 | """ Convert image to ASCII art with color """
51 |
52 |
53 | response = requests.get(image_url, timeout=10)
54 | self.log.debug("[green]Converting image to ASCII")
55 | image = Image.open(BytesIO(response.content))
56 | aspect_ratio = image.height / image.width
57 | new_height = int(width * aspect_ratio * 0.5)
58 | resized_image = image.resize((width, new_height))
59 | pixels = resized_image.convert("RGB").load()
60 | ascii_art = ""
61 |
62 | for y in range(resized_image.height):
63 | for x in range(resized_image.width):
64 | r, g, b, *_ = pixels[x, y]
65 | char = "\u2588" if eval(self.CONFIG["ASCII_IMAGE_UNICODE"]) else "@"
66 | ascii_art += self.rgb_to_ansi(r, g, b) + char
67 | if y != resized_image.height - 1:
68 | ascii_art += "\n"
69 |
70 | ascii_art += "\x1b[0m"
71 |
72 | return ascii_art
73 |
--------------------------------------------------------------------------------
/spotrpy/parser/parser.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import textwrap
3 | import inspect
4 |
5 | from spotrpy.commands import Command
6 | from spotrpy.assets.LOGO import SPOTR_LOGO
7 |
8 | from spotrpy.logging.Logging import Logging
9 |
10 | """
11 | Initializes ArgumentParser and parses commands from commands.py then runs given method
12 | """
13 |
14 | class Parser(Logging):
15 | """ Parser """
16 |
17 | def __init__(self) -> None:
18 | """ Overview of entire command execution """
19 |
20 | self.parser = argparse.ArgumentParser(description="Spotr Command Line Interface")
21 | self.global_args()
22 | self.subParsers()
23 |
24 | super().__initLogging__()
25 | self.log.debug(self.args)
26 |
27 | self.exec()
28 | self.postExec()
29 |
30 | def global_args(self) -> None:
31 | """ Universal arguments """
32 |
33 | self.parser.add_argument("-d", "--debug", help="Run in debug", action="store_true")
34 | self.parser.add_argument("-c", "--current", help="Run current, post exec", action="store_true")
35 | self.parser.add_argument("-r", "--response", help="Dump response", action="store_true")
36 |
37 | def subParsers(self) -> None:
38 | """ Parse defined commands and add to ArgumentParser """
39 |
40 | subparser = self.parser.add_subparsers(dest='command')
41 |
42 | for command in Command.commands:
43 | command_parser = subparser.add_parser(
44 | Command.commands[command]['name'],
45 | help=Command.commands[command]['desc'],
46 | description=Command.commands[command]['desc']
47 | )
48 |
49 | if Command.commands[command]['options']:
50 | for option in Command.commands[command]['options']:
51 | command_parser.add_argument(
52 | *option['flags'],
53 | **option['kwargs']
54 | )
55 |
56 | self.args = self.parser.parse_args()
57 |
58 | def exec(self, command=None) -> None:
59 | """ Command execution logic """
60 |
61 | if command is None: command = self.args.command
62 |
63 | if self.args.command:
64 | self.log.debug(f"Executing {Command.commands[command]['callable']}")
65 |
66 | callable = Command.commands[command]['callable']
67 |
68 | if isinstance(callable, list):
69 | controller = callable[0](self.args)
70 | getattr(controller, callable[1])()
71 | elif inspect.isfunction(callable):
72 | Command.commands[command]['callable'](self.args)
73 | else:
74 | self.log.error("Callable must be of type class or function")
75 | else:
76 | print(textwrap.indent(SPOTR_LOGO, " " * 2))
77 | self.parser.print_help()
78 |
79 | def postExec(self) -> None:
80 | """ Post execution logic """
81 |
82 | if self.args.current: self.exec("current")
83 |
84 |
85 |
--------------------------------------------------------------------------------
/spotrpy/config/config.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import json
4 |
5 | class Config():
6 | """ Config """
7 |
8 | def __initConfig__(self):
9 | try:
10 | with open(
11 | os.path.join(
12 | os.path.dirname(os.path.realpath(__file__)), "config.json"
13 | ),
14 | "r",
15 | encoding="utf-8",
16 | ) as file:
17 | self.CONFIG = json.load(file)
18 | self.validate_config()
19 | except FileNotFoundError:
20 | self.log.critical("Config file not found")
21 | create_file = str(input("Create config? y/n: "))
22 | if create_file.lower() == "y":
23 | self.CONFIG = {
24 | "path": os.path.dirname(os.path.realpath(__file__)),
25 | "refresh_token": "",
26 | "base_64": "",
27 | "key": "",
28 | "API_PROCESS_DELAY": "2",
29 | "ANSI_COLORS": "True",
30 | "USE_ASCII_LOGO": "True",
31 | "LOG_TRACK_URIS": "True",
32 | "ASCII_IMAGE": "True",
33 | "ASCII_IMAGE_SIZE_WIDTH": "50",
34 | "ASCII_IMAGE_COLOR": "True",
35 | "ASCII_IMAGE_UNICODE": "True",
36 | "ASCII_IMAGE_CHARS": '@%#*+=-:.`^",:;Il!i~+_-?][}{1)(|\\/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$',
37 | "PRINT_DELAY_ACTIVE": "True",
38 | "PRINT_DELAY": "0.01",
39 | }
40 | with open(
41 | os.path.join(
42 | os.path.dirname(os.path.realpath(__file__)), "config.json"
43 | ),
44 | "w",
45 | encoding="utf-8",
46 | ) as file:
47 | json.dump(self.CONFIG, file, indent=4)
48 | else:
49 | sys.exit()
50 |
51 | def write_config(self):
52 | """ Write config data """
53 |
54 | with open(
55 | os.path.join(self.CONFIG["path"], "config.json"), "w", encoding="utf-8"
56 | ) as file:
57 | json.dump(self.CONFIG, file, indent=4)
58 |
59 | def validate_config(self):
60 | """ Validate config """
61 |
62 | required_keys = [
63 | "path",
64 | "refresh_token",
65 | "base_64",
66 | "key",
67 | "API_PROCESS_DELAY",
68 | "ANSI_COLORS",
69 | "USE_ASCII_LOGO",
70 | "LOG_TRACK_URIS",
71 | "ASCII_IMAGE",
72 | "ASCII_IMAGE_SIZE_WIDTH",
73 | "ASCII_IMAGE_COLOR",
74 | "ASCII_IMAGE_UNICODE",
75 | "ASCII_IMAGE_CHARS",
76 | "PRINT_DELAY_ACTIVE",
77 | "PRINT_DELAY",
78 | ]
79 | config_keys = set(self.CONFIG.keys())
80 |
81 | if not set(required_keys).issubset(config_keys):
82 | missing_keys = set(required_keys) - config_keys
83 | self.log.error("The following keys are missing in the config.json: %s", ", ".join(missing_keys))
84 | sys.exit()
85 |
86 | return
87 |
--------------------------------------------------------------------------------
/spotrpy/controllers/searchController.py:
--------------------------------------------------------------------------------
1 | import questionary
2 | from urllib.parse import urljoin, urlencode
3 |
4 | from spotrpy.core.BaseController import BaseController
5 | from spotrpy.util.Helpers import Helpers
6 |
7 |
8 | class searchController(BaseController, Helpers):
9 |
10 | def run(self):
11 | search_types = {
12 | "track": {
13 | "accessor": ["tracks", "items"],
14 | "return_value": ["uri"],
15 | "name_value": ["name"],
16 | "artists_value": ["artists"],
17 | "artists_array": True,
18 | "json_value": "uris",
19 | "json_value_array": True,
20 | "json": {"uris": []},
21 | },
22 | "playlist": {
23 | "accessor": ["playlists", "items"],
24 | "return_value": ["uri"],
25 | "name_value": ["name"],
26 | "artists_value": ["owner", "display_name"],
27 | "artists_array": False,
28 | "json_value": "context_uri",
29 | "json_value_array": False,
30 | "json": {"context_uri": [], "offset": {"position": "0"}},
31 | },
32 | "album": {
33 | "accessor": ["albums", "items"],
34 | "return_value": ["uri"],
35 | "name_value": ["name"],
36 | "artists_value": ["artists"],
37 | "artists_array": True,
38 | "json_value": "context_uri",
39 | "json_value_array": False,
40 | "json": {"context_uri": [], "offset": {"position": "0"}},
41 | },
42 | }
43 |
44 | if not self.args.type:
45 | available_types = ["track", "playlist", "album"]
46 | search_type = questionary.select(
47 | "Select search type",
48 | choices=available_types,
49 | erase_when_done=True,
50 | use_shortcuts=True,
51 | use_arrow_keys=True,
52 | use_jk_keys=False,
53 | ).ask()
54 | if search_type is None:
55 | return
56 | else:
57 | search_type = self.args.type
58 |
59 | self.fetch(search_type)
60 |
61 | choices = self.parse_items(
62 | self.response,
63 | accessor=search_types[search_type]["accessor"],
64 | return_value=search_types[search_type]["return_value"],
65 | name_value=search_types[search_type]["name_value"],
66 | artists_value=search_types[search_type]["artists_value"],
67 | artists_array=search_types[search_type]["artists_array"],
68 | )
69 |
70 | selected = questionary.select(
71 | "What song do you want to play?",
72 | choices=choices,
73 | erase_when_done=True,
74 | use_shortcuts=True,
75 | use_arrow_keys=True,
76 | use_jk_keys=False,
77 | ).ask()
78 |
79 | if selected is None:
80 | return
81 |
82 | json = search_types[search_type]["json"]
83 | if search_types[search_type]["json_value_array"]:
84 | json[search_types[search_type]["json_value"]] = [selected]
85 | else:
86 | json[search_types[search_type]["json_value"]] = selected
87 |
88 | self.request("PUT", str(urljoin(self.API_PLAYER, "play")), json=json)
89 |
90 | def fetch(self, search_type):
91 | self.response = self.request(
92 | "GET",
93 | str(
94 | f"{urljoin(self.API_BASE_VERSION, 'search')}?{urlencode({'q': ' '.join(self.args.query), 'type': search_type, 'limit': self.QUSTIONARY_LIMIT})}"
95 | )
96 | )
97 |
98 |
99 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
Spotr - A spotify tool for the terminal
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | A very simple CLI for controlling your spotify on the fly in the terminal. Made in python for simplicity
19 |
20 |
21 |
22 |
23 | Debug
24 |
25 |
26 |
27 | Argparser
28 |
29 |
30 |
31 | Installation
32 |
33 | ```
34 | $ pip install spotrpy
35 | ```
36 | Or clone the repo and install locally
37 | ```
38 | $ pip install -e .
39 | ```
40 |
41 | 2. Register an app in spotify for developers "https://developer.spotify.com/dashboard/applications"
42 | - Also set callback URI to "https://www.google.com/"
43 |
44 | 3. Run any spotr command (If command is not recognized check if python bin is in PATH)
45 |
46 | 4. You will be prompted to create config.json and paste in client and secret id from spotify app
47 |
48 | 5. After these steps everything should work and you can enjoy spotr
49 |
50 |
51 |
52 | Built-in Commands
53 |
54 | auth Authorize Spotify api
55 | authorize Authorize Spotify api
56 | artist Display artist information
57 | ascii Ascii image for current track
58 | current Display information about the currently playing track
59 | next Play next track
60 | playback Set playback state
61 | playlist Start playing one of your playlists
62 | playlistadd Add currently playing track to a playlist
63 | previous Play previous track
64 | qsearch Quicksearch for tracks
65 | queue Display current queue
66 | recent Select one of recently played tracks
67 | recommend Play random / recommended tracks based on recent tracks
68 | replay Replay/Restart currently playing track
69 | search Search for anything on spotify, Types - track, playlist, album
70 | seek Seek posistion for track (in seconds)
71 | shuffle Toggle shuffle, on / off
72 | start Start/resume playing
73 | resume Start/resume playing
74 | stop Stop/Pause playing
75 | pause Stop/Pause playing
76 | volume Ajust volume
77 | vol Ajust volume
78 | web Open currently playing track in a broswer
79 |
80 | Modifications
81 |
82 |
83 | Commands are declared in commands.py, Then you can set any function or class-method as callable.
84 |
85 |
86 |
87 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/spotrpy/controllers/artistController.py:
--------------------------------------------------------------------------------
1 | import re
2 | import sys
3 | import time
4 | import textwrap
5 | import questionary
6 | from urllib.parse import urljoin, urlencode
7 |
8 | from spotrpy.core.BaseController import BaseController
9 | from spotrpy.util.ASCII import ASCII
10 | from spotrpy.util.Helpers import Helpers
11 |
12 |
13 | class artistController (BaseController, Helpers, ASCII):
14 |
15 | def run(self):
16 | if self.args.artist:
17 | self.response = self.request(
18 | "GET",
19 | str(
20 | f"{urljoin(self.API_BASE_VERSION, 'search')}?{urlencode({'q': ' '.join(self.args.artist), 'type': 'artist', 'limit': self.QUSTIONARY_LIMIT})}"
21 | )
22 | )
23 | else:
24 | current = self.request("GET", urljoin(self.API_PLAYER, "currently-playing"))
25 | if current is None:
26 | self.log.error("No track currently playing")
27 | sys.exit()
28 | self.response = self.request("GET", urljoin(self.API_BASE_VERSION, f"artists/{current['item']['artists'][0]['id']}"))
29 |
30 | if self.args.artist:
31 | choices = self.parse_items(
32 | self.response,
33 | accessor=(["artists", "items"] if self.args.artist else None),
34 | name_value=["name"],
35 | artists_value=False,
36 | )
37 |
38 | selected = questionary.select(
39 | "Select artist",
40 | choices=choices,
41 | erase_when_done=True,
42 | use_shortcuts=True,
43 | use_arrow_keys=True,
44 | use_jk_keys=False,
45 | ).ask()
46 | if selected is None:
47 | return
48 | else:
49 | selected = self.response
50 |
51 | color_start = "\x1b[{}m".format
52 | color_end = "\x1b[0m"
53 |
54 | strings = textwrap.dedent(
55 | f"""
56 | {color_start('31')}Artist{color_end}
57 | {color_start('32')}------------------------------{color_end}
58 | {color_start('37')}Name{color_end}{color_start('32')} - {selected['name']}{color_end}
59 | {color_start('37')}Popularity{color_end}{color_start('32')} - {selected['popularity']}{color_end}
60 | {color_start('37')}Genres{color_end}{color_start('32')} - {", ".join(selected['genres'])}{color_end}
61 | {color_start('37')}Followers{color_end}{color_start('32')} - {selected['followers']['total']}{color_end}
62 |
63 | {color_start('31')}Details{color_end}
64 | {color_start('32')}------------------------------{color_end}
65 | {color_start('37')}URL{color_end}{color_start('32')} - {selected['href']}{color_end}
66 | {color_start('37')}URI{color_end}{color_start('32')} - {selected['uri']}{color_end}
67 | {color_start('37')}Image{color_end}{color_start('32')} - {selected['images'][0]['url']}{color_end}
68 | """
69 | )
70 | ansi_color_escape = re.compile(r"\x1b\[\d{1,2}m")
71 | strings_no_color = ansi_color_escape.sub("", strings)
72 | if not eval(self.CONFIG["ANSI_COLORS"]):
73 | strings = strings_no_color
74 |
75 | strings = strings.strip().splitlines()
76 |
77 | if eval(self.CONFIG["ASCII_IMAGE"]) or eval(self.CONFIG["USE_ASCII_LOGO"]):
78 | if eval(self.CONFIG["ASCII_IMAGE"]):
79 | width = self.CONFIG["ASCII_IMAGE_SIZE_WIDTH"]
80 | ascii_str = (
81 | self.image_to_ascii_color(
82 | selected["images"][0]["url"], int(width)
83 | )
84 | if eval(self.CONFIG["ASCII_IMAGE_COLOR"])
85 | else self.image_to_ascii(
86 | selected["images"][0]["url"], int(width)
87 | )
88 | ).splitlines()
89 | elif eval(self.CONFIG["USE_ASCII_LOGO"]):
90 | ascii_str = self.CONFIG["ASCII_LOGO"]
91 | print()
92 | for i, line in enumerate(ascii_str):
93 | if eval(self.CONFIG["PRINT_DELAY_ACTIVE"]):
94 | time.sleep(float(self.CONFIG["PRINT_DELAY"]))
95 | print(f" {line} {strings[i] if i < len(strings) else ''}")
96 | print()
97 | else:
98 | print()
99 | for line in strings:
100 | if eval(self.CONFIG["PRINT_DELAY_ACTIVE"]):
101 | time.sleep(float(self.CONFIG["PRINT_DELAY"]))
102 | print(f" {line}")
103 | print()
104 |
105 |
--------------------------------------------------------------------------------
/spotrpy/api/API.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import sys
3 | import requests
4 |
5 | from urllib.parse import urljoin
6 |
7 | class API():
8 | """ API """
9 |
10 | def __initAPI__(self):
11 | self.SPOTIFY_LIMIT = 50
12 | self.QUSTIONARY_LIMIT = 36
13 | self.API_VERSION = "v1"
14 | self.API_BASE = "api.spotify.com"
15 | self.ACCOUNT_URL = "https://accounts.spotify.com"
16 | self.API_PLAYER = urljoin("https://api.spotify.com", f"{self.API_VERSION}/me/player/")
17 | self.API_BASE_VERSION = urljoin("https://api.spotify.com", f"{self.API_VERSION}/")
18 |
19 | def request(
20 | self,
21 | method,
22 | url,
23 | headers=None,
24 | json=None
25 | ):
26 | """ Spotify API request """
27 |
28 | if headers is None: headers = {"Authorization": f"Bearer {self.CONFIG['key']}"}
29 |
30 | response = requests.request(method, url, headers=headers, json=json, timeout=10)
31 |
32 | if not response.ok:
33 | status_code = response.status_code
34 |
35 | self.log.error(f"Requst error - {status_code}")
36 |
37 | if status_code in (401, 400):
38 | self.__refresh_token()
39 |
40 | headers = {"Authorization": f"Bearer {self.CONFIG['key']}"}
41 | response = requests.request(method, url, headers=headers, json=json, timeout=10)
42 |
43 | try:
44 | data = response.json()
45 | except ValueError:
46 | data = None
47 |
48 | if self.args.response:
49 | self.log.info(data)
50 |
51 | return data
52 |
53 | def __refresh_token(self):
54 | """ Refresh Spotify API token """
55 |
56 | self.log.debug(f"Refreshing API-Token")
57 | url = urljoin(self.ACCOUNT_URL, "api/token")
58 |
59 | response = requests.post(
60 | url,
61 | data={
62 | "grant_type": "refresh_token",
63 | "refresh_token": self.CONFIG["refresh_token"],
64 | },
65 | headers={"Authorization": "Basic " + self.CONFIG["base_64"]},
66 | timeout=10,
67 | )
68 |
69 | if not response.ok:
70 | self.log.critical(f"Request error - status-code: {response.status_code}")
71 | self.log.critical("Indication of invalid refresh_token, or base_64")
72 |
73 | sys.exit()
74 |
75 | data = response.json()
76 | self.CONFIG["key"] = data["access_token"]
77 | self.write_config()
78 |
79 | def authorise(self, client_id=None, client_secret=None):
80 | """ Authorise CLI with Spotify API """
81 |
82 | auth_url = urljoin(self.ACCOUNT_URL, "authorize")
83 | token_url = urljoin(self.ACCOUNT_URL, "api/token")
84 |
85 | if not client_id: client_id = str(input("Spotify-App Client id: "))
86 | if not client_secret: client_secret = str(input("Spotify-App Client secret: "))
87 |
88 | auth_request = requests.get(
89 | auth_url,
90 | {
91 | "client_id": client_id,
92 | "response_type": "code",
93 | "redirect_uri": "https://www.google.com/",
94 | "scope": "playlist-read-collaborative playlist-read-private user-read-playback-state user-modify-playback-state user-read-currently-playing user-read-recently-played playlist-modify-private playlist-modify-public",
95 | },
96 | timeout=10,
97 | )
98 |
99 | print("\nGo to the Following URL, Accept the terms, Copy the code in the redirected URL, Then paste the code into the terminal\n")
100 | print(f"\n{auth_request.url}\n")
101 |
102 | auth_code = str(input("Enter code from the URL: "))
103 | client_creds = f"{client_id}:{client_secret}"
104 | client_creds_b64 = base64.b64encode(client_creds.encode())
105 |
106 | headers = {
107 | "Content-Type": "application/x-www-form-urlencoded",
108 | "Authorization": "Basic %s" % client_creds_b64.decode(),
109 | }
110 | payload = {
111 | "grant_type": "authorization_code",
112 | "code": auth_code,
113 | "redirect_uri": "https://www.google.com/",
114 | }
115 |
116 | access_token_request = requests.post(
117 | url=token_url, data=payload, headers=headers, timeout=10
118 | )
119 |
120 | if not access_token_request.ok:
121 | self.log.warning("Request error: %d", access_token_request.status_code)
122 | sys.exit()
123 |
124 | access_token_response_data = access_token_request.json()
125 | self.CONFIG["refresh_token"] = access_token_response_data["refresh_token"]
126 | self.CONFIG["base_64"] = client_creds_b64.decode()
127 |
128 | self.write_config()
129 | print("All done!")
130 |
--------------------------------------------------------------------------------
/spotrpy/controllers/currentController.py:
--------------------------------------------------------------------------------
1 | import re
2 | import time
3 | import sys
4 | import textwrap
5 | from urllib.parse import urljoin
6 |
7 | from spotrpy.core.BaseController import BaseController
8 | from spotrpy.util.ASCII import ASCII
9 |
10 | class currentController(BaseController, ASCII):
11 |
12 | def run(self):
13 | self.fetch()
14 |
15 | current_track = self.response["item"]
16 | album_data = current_track["album"]
17 | artist_names = ", ".join([artist["name"] for artist in current_track["artists"]])
18 | track_duration_ms = current_track["duration_ms"]
19 | track_duration_m, track_duration_s = divmod(track_duration_ms // 1000, 60)
20 | progress_ms = self.response["progress_ms"]
21 | progress_m, progress_s = divmod(progress_ms // 1000, 60)
22 | track_id = current_track["id"]
23 | track_name = current_track["name"]
24 | track_type = album_data["album_type"]
25 | album_name = album_data["name"]
26 | track_release_date = album_data["release_date"]
27 | track_url = current_track["external_urls"]["spotify"]
28 | track_image = album_data["images"][0]["url"]
29 |
30 | color_start = "\x1b[{}m".format
31 | color_end = "\x1b[0m"
32 |
33 | strings = textwrap.dedent(
34 | f"""
35 | {color_start('31')}Current track{color_end}
36 | {color_start('32')}------------------------------{color_end}
37 | {color_start('37')}Name{color_end}{color_start('32')} - {track_name}{color_end}
38 | {color_start('37')}Artits{color_end}{color_start('32')} - {artist_names}{color_end}
39 | {color_start('37')}Duration{color_end}{color_start('32')} - {track_duration_m} minutes {track_duration_s} seconds{color_end}
40 | {color_start('37')}Progress{color_end}{color_start('32')} - {progress_m} minutes {progress_s} seconds{color_end}
41 | {color_start('37')}Release date{color_end}{color_start('32')} - {track_release_date}{color_end}
42 | {color_start('37')}From{color_end}{color_start('32')} - {track_type} - {album_name}{color_end}
43 |
44 | {color_start('31')}Track details{color_end}
45 | {color_start('32')}------------------------------{color_end}
46 | {color_start('37')}Id{color_end}{color_start('32')} - {track_id}{color_end}
47 | {color_start('37')}URL{color_end}{color_start('32')} - {track_url}{color_end}
48 | {color_start('37')}Image{color_end}{color_start('32')} - {track_image}{color_end}
49 | """
50 | )
51 |
52 | ansi_color_escape = re.compile(r"\x1b\[\d{1,2}m")
53 | strings_no_color = ansi_color_escape.sub("", strings)
54 | if not eval(self.CONFIG["ANSI_COLORS"]):
55 | strings = strings_no_color
56 |
57 | strings = strings.strip().splitlines()
58 |
59 | if eval(self.CONFIG["ASCII_IMAGE"]) or eval(self.CONFIG["USE_ASCII_LOGO"]):
60 | if eval(self.CONFIG["ASCII_IMAGE"]):
61 | width = self.CONFIG["ASCII_IMAGE_SIZE_WIDTH"]
62 | ascii_str = (
63 | self.image_to_ascii_color(
64 | self.response["item"]["album"]["images"][0]["url"], int(width)
65 | )
66 | if eval(self.CONFIG["ASCII_IMAGE_COLOR"])
67 | else self.image_to_ascii(
68 | self.response["item"]["album"]["images"][0]["url"], int(width)
69 | )
70 | ).splitlines()
71 | elif eval(self.CONFIG["USE_ASCII_LOGO"]):
72 | ascii_str = self.CONFIG["ASCII_LOGO"]
73 | print()
74 | for i, line in enumerate(ascii_str):
75 | if eval(self.CONFIG["PRINT_DELAY_ACTIVE"]):
76 | time.sleep(float(self.CONFIG["PRINT_DELAY"]))
77 | print(f" {line} {strings[i] if i < len(strings) else ''}")
78 | print()
79 | else:
80 | print()
81 | for line in strings:
82 | if eval(self.CONFIG["PRINT_DELAY_ACTIVE"]):
83 | time.sleep(float(self.CONFIG["PRINT_DELAY"]))
84 | print(f" {line}")
85 | print()
86 |
87 | def fetch(self):
88 | response = self.request("GET", urljoin(self.API_PLAYER, "currently-playing"))
89 |
90 | if response is None or response["item"] is None:
91 | self.log.error(f'No data - {response}')
92 | sys.exit()
93 | elif response["currently_playing_type"] not in ("track"):
94 | self.log.error("Playing unsupported type - %s", response['currently_playing_type'])
95 | sys.exit()
96 | else:
97 | self.response = response
98 |
99 |
100 |
--------------------------------------------------------------------------------
/spotrpy/commands.py:
--------------------------------------------------------------------------------
1 | from spotrpy.interface.Command import Command
2 |
3 | from spotrpy.controllers.artistController import artistController
4 | from spotrpy.controllers.previousController import previousController
5 | from spotrpy.controllers.qsearchController import qsearchController
6 | from spotrpy.controllers.queueController import queueController
7 | from spotrpy.controllers.recentController import recentController
8 | from spotrpy.controllers.recommendController import recommendController
9 | from spotrpy.controllers.replayController import replayController
10 | from spotrpy.controllers.searchController import searchController
11 | from spotrpy.controllers.seekController import seekController
12 | from spotrpy.controllers.shuffleController import shuffleController
13 | from spotrpy.controllers.startController import startController
14 | from spotrpy.controllers.stopController import stopController
15 | from spotrpy.controllers.volumeController import volumeController
16 | from spotrpy.controllers.webController import webController
17 | from spotrpy.controllers.asciiController import asciiController
18 | from spotrpy.controllers.nextController import nextController
19 | from spotrpy.controllers.playbackController import playbackController
20 | from spotrpy.controllers.playlistController import playlistController
21 | from spotrpy.controllers.playlistaddController import playlistaddController
22 | from spotrpy.controllers.authController import authController
23 | from spotrpy.controllers.currentController import currentController
24 |
25 | Command = Command()
26 |
27 | """
28 | Define all commands
29 |
30 | Command.command takes following parameters:
31 | [
32 | 'names': List -- name(s) / callword(s) for command,
33 | 'description': str -- command description for argparser,
34 | 'callable': Callable | List[Callable, 'metod'] -- callable to initialize / run on command execution (callable must accept 'args' parameter),
35 | 'options': List *optional -- options passed to 'add_argument' function for command subparser (contains 'flags' and 'kwargs'),
36 | ]
37 | """
38 |
39 | ### AUTH
40 |
41 | Command.command(["auth", "authorize"], "Authorize Spotify api", [authController, "auth"], options=[
42 | {'flags': ['-ci', '--client_id'], 'kwargs': {'type': str, 'help': 'Your spotify-app client-id'}},
43 | {'flags': ['-cs', '--client_secret'], 'kwargs': {'type': str, 'help': 'Your spotify-app client-secret'}}
44 | ])
45 |
46 | ### TESTING
47 |
48 | #Command.command(["hello"], "Terminal says hello", lambda args: print("hello!"))
49 |
50 | #def world(args):
51 | # print("world!")
52 | #Command.command(["world"], "function", world)
53 |
54 | ### SPOTR
55 |
56 | Command.command(["artist"], "Display artist information", [artistController, "run"], options=[
57 | {'flags': ['artist'], 'kwargs': {'type': str, 'help': 'Optionally search for artist', 'nargs': '*'}}
58 | ])
59 | Command.command(["ascii"], "Ascii image for current track", [asciiController, "run"], options=[
60 | {'flags': ['-w', '--width'], 'kwargs': {'type': str, 'help': 'Set ascii image width'}}
61 | ])
62 | Command.command(["current"], "Display information about the currently playing track", [currentController, "run"])
63 | Command.command(["next"], "Play next track", [nextController, "run"])
64 | Command.command(["playback"], "Set playback state", [playbackController, "run"], options=[
65 | {'flags': ['-s', '--state'], 'kwargs': {'type': str, 'choices': ['track', 'context', 'off'], 'help': 'playback state'}}
66 | ])
67 | Command.command(["playlist"], "Start playing one of your playlists", [playlistController, "run"])
68 | Command.command(["playlistadd"], "Add currently playing track to a playlist", [playlistaddController, "run"])
69 | Command.command(["previous"], "Play previous track", [previousController, "run"])
70 | Command.command(["qsearch"], "Quicksearch for tracks", [qsearchController, "run"], options=[
71 | {'flags': ['query'], 'kwargs': {'type': str, 'help': 'Search query', 'nargs': '*'}}
72 | ])
73 | Command.command(["queue"], "Display current queue", [queueController, "run"])
74 | Command.command(["recent"], "Select one of recently played tracks", [recentController, "run"])
75 | Command.command(["recommend"], "Play random / recommended tracks based on recent tracks", [recommendController, "run"])
76 | Command.command(["replay"], "Replay/Restart currently playing track", [replayController, "run"])
77 | Command.command(["search"], "Search for anything on spotify, Types - track, playlist, album", [searchController, "run"], options=[
78 | {'flags': ['query'], 'kwargs': {'type': str, 'help': 'Search query', 'nargs': '*'}},
79 | {'flags': ['-t', '--type'], 'kwargs': {'type': str, 'choices': ['track', 'playlist', 'album'], 'help': 'Search type'}}
80 | ])
81 | Command.command(["seek"], "Seek posistion for track (in seconds)", [seekController, "run"], options=[
82 | {'flags': ['seconds'], 'kwargs': {'type': str, 'help': 'Song posistion'}},
83 | ])
84 | Command.command(["shuffle"], "Toggle shuffle, on / off", [shuffleController, "run"], options=[
85 | {'flags': ['-s', '--state'], 'kwargs': {'type': str, 'choices': ['true', 'false'], 'help': 'Toggle shuffle'}}
86 | ])
87 | Command.command(["start", "resume"], "Start/resume playing", [startController, "run"])
88 | Command.command(["stop", "pause"], "Stop/Pause playing", [stopController, "run"])
89 | Command.command(["volume", "vol"], "Ajust volume", [volumeController, "run"], options=[
90 | {'flags': ['-p', '--percentage'], 'kwargs': {'type': str, 'help': 'Volume percentage'}}
91 | ])
92 | Command.command(["web"], "Open currently playing track in a broswer", [webController, "run"])
93 |
94 |
--------------------------------------------------------------------------------
/spotrpy/.pylintrc:
--------------------------------------------------------------------------------
1 | [MAIN]
2 |
3 | # Analyse import fallback blocks. This can be used to support both Python 2 and
4 | # 3 compatible code, which means that the block might have code that exists
5 | # only in one or another interpreter, leading to false positives when analysed.
6 | analyse-fallback-blocks=no
7 |
8 | # Clear in-memory caches upon conclusion of linting. Useful if running pylint
9 | # in a server-like mode.
10 | clear-cache-post-run=no
11 |
12 | # Load and enable all available extensions. Use --list-extensions to see a list
13 | # all available extensions.
14 | #enable-all-extensions=
15 |
16 | # In error mode, messages with a category besides ERROR or FATAL are
17 | # suppressed, and no reports are done by default. Error mode is compatible with
18 | # disabling specific errors.
19 | #errors-only=
20 |
21 | # Always return a 0 (non-error) status code, even if lint errors are found.
22 | # This is primarily useful in continuous integration scripts.
23 | #exit-zero=
24 |
25 | # A comma-separated list of package or module names from where C extensions may
26 | # be loaded. Extensions are loading into the active Python interpreter and may
27 | # run arbitrary code.
28 | extension-pkg-allow-list=
29 |
30 | # A comma-separated list of package or module names from where C extensions may
31 | # be loaded. Extensions are loading into the active Python interpreter and may
32 | # run arbitrary code. (This is an alternative name to extension-pkg-allow-list
33 | # for backward compatibility.)
34 | extension-pkg-whitelist=
35 |
36 | # Return non-zero exit code if any of these messages/categories are detected,
37 | # even if score is above --fail-under value. Syntax same as enable. Messages
38 | # specified are enabled, while categories only check already-enabled messages.
39 | fail-on=
40 |
41 | # Specify a score threshold under which the program will exit with error.
42 | fail-under=10
43 |
44 | # Interpret the stdin as a python script, whose filename needs to be passed as
45 | # the module_or_package argument.
46 | #from-stdin=
47 |
48 | # Files or directories to be skipped. They should be base names, not paths.
49 | ignore=CVS
50 |
51 | # Add files or directories matching the regular expressions patterns to the
52 | # ignore-list. The regex matches against paths and can be in Posix or Windows
53 | # format. Because '\\' represents the directory delimiter on Windows systems,
54 | # it can't be used as an escape character.
55 | ignore-paths=
56 |
57 | # Files or directories matching the regular expression patterns are skipped.
58 | # The regex matches against base names, not paths. The default value ignores
59 | # Emacs file locks
60 | ignore-patterns=^\.#
61 |
62 | # List of module names for which member attributes should not be checked
63 | # (useful for modules/projects where namespaces are manipulated during runtime
64 | # and thus existing member attributes cannot be deduced by static analysis). It
65 | # supports qualified module names, as well as Unix pattern matching.
66 | ignored-modules=
67 |
68 | # Python code to execute, usually for sys.path manipulation such as
69 | # pygtk.require().
70 | #init-hook=
71 |
72 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
73 | # number of processors available to use, and will cap the count on Windows to
74 | # avoid hangs.
75 | jobs=1
76 |
77 | # Control the amount of potential inferred values when inferring a single
78 | # object. This can help the performance when dealing with large functions or
79 | # complex, nested conditions.
80 | limit-inference-results=100
81 |
82 | # List of plugins (as comma separated values of python module names) to load,
83 | # usually to register additional checkers.
84 | load-plugins=
85 |
86 | # Pickle collected data for later comparisons.
87 | persistent=yes
88 |
89 | # Minimum Python version to use for version dependent checks. Will default to
90 | # the version used to run pylint.
91 | py-version=3.11
92 |
93 | # Discover python modules and packages in the file system subtree.
94 | recursive=no
95 |
96 | # Add paths to the list of the source roots. Supports globbing patterns. The
97 | # source root is an absolute path or a path relative to the current working
98 | # directory used to determine a package namespace for modules located under the
99 | # source root.
100 | source-roots=
101 |
102 | # When enabled, pylint would attempt to guess common misconfiguration and emit
103 | # user-friendly hints instead of false-positive error messages.
104 | suggestion-mode=yes
105 |
106 | # Allow loading of arbitrary C extensions. Extensions are imported into the
107 | # active Python interpreter and may run arbitrary code.
108 | unsafe-load-any-extension=no
109 |
110 | # In verbose mode, extra non-checker-related info will be displayed.
111 | #verbose=
112 |
113 |
114 | [BASIC]
115 |
116 | # Naming style matching correct argument names.
117 | argument-naming-style=snake_case
118 |
119 | # Regular expression matching correct argument names. Overrides argument-
120 | # naming-style. If left empty, argument names will be checked with the set
121 | # naming style.
122 | #argument-rgx=
123 |
124 | # Naming style matching correct attribute names.
125 | attr-naming-style=snake_case
126 |
127 | # Regular expression matching correct attribute names. Overrides attr-naming-
128 | # style. If left empty, attribute names will be checked with the set naming
129 | # style.
130 | #attr-rgx=
131 |
132 | # Bad variable names which should always be refused, separated by a comma.
133 | bad-names=foo,
134 | bar,
135 | baz,
136 | toto,
137 | tutu,
138 | tata
139 |
140 | # Bad variable names regexes, separated by a comma. If names match any regex,
141 | # they will always be refused
142 | bad-names-rgxs=
143 |
144 | # Naming style matching correct class attribute names.
145 | class-attribute-naming-style=any
146 |
147 | # Regular expression matching correct class attribute names. Overrides class-
148 | # attribute-naming-style. If left empty, class attribute names will be checked
149 | # with the set naming style.
150 | #class-attribute-rgx=
151 |
152 | # Naming style matching correct class constant names.
153 | class-const-naming-style=UPPER_CASE
154 |
155 | # Regular expression matching correct class constant names. Overrides class-
156 | # const-naming-style. If left empty, class constant names will be checked with
157 | # the set naming style.
158 | #class-const-rgx=
159 |
160 | # Naming style matching correct class names.
161 | class-naming-style=PascalCase
162 |
163 | # Regular expression matching correct class names. Overrides class-naming-
164 | # style. If left empty, class names will be checked with the set naming style.
165 | #class-rgx=
166 |
167 | # Naming style matching correct constant names.
168 | const-naming-style=UPPER_CASE
169 |
170 | # Regular expression matching correct constant names. Overrides const-naming-
171 | # style. If left empty, constant names will be checked with the set naming
172 | # style.
173 | #const-rgx=
174 |
175 | # Minimum line length for functions/classes that require docstrings, shorter
176 | # ones are exempt.
177 | docstring-min-length=-1
178 |
179 | # Naming style matching correct function names.
180 | function-naming-style=snake_case
181 |
182 | # Regular expression matching correct function names. Overrides function-
183 | # naming-style. If left empty, function names will be checked with the set
184 | # naming style.
185 | #function-rgx=
186 |
187 | # Good variable names which should always be accepted, separated by a comma.
188 | good-names=i,
189 | j,
190 | k,
191 | ex,
192 | Run,
193 | _
194 |
195 | # Good variable names regexes, separated by a comma. If names match any regex,
196 | # they will always be accepted
197 | good-names-rgxs=
198 |
199 | # Include a hint for the correct naming format with invalid-name.
200 | include-naming-hint=no
201 |
202 | # Naming style matching correct inline iteration names.
203 | inlinevar-naming-style=any
204 |
205 | # Regular expression matching correct inline iteration names. Overrides
206 | # inlinevar-naming-style. If left empty, inline iteration names will be checked
207 | # with the set naming style.
208 | #inlinevar-rgx=
209 |
210 | # Naming style matching correct method names.
211 | method-naming-style=snake_case
212 |
213 | # Regular expression matching correct method names. Overrides method-naming-
214 | # style. If left empty, method names will be checked with the set naming style.
215 | #method-rgx=
216 |
217 | # Naming style matching correct module names.
218 | module-naming-style=snake_case
219 |
220 | # Regular expression matching correct module names. Overrides module-naming-
221 | # style. If left empty, module names will be checked with the set naming style.
222 | #module-rgx=
223 |
224 | # Colon-delimited sets of names that determine each other's naming style when
225 | # the name regexes allow several styles.
226 | name-group=
227 |
228 | # Regular expression which should only match function or class names that do
229 | # not require a docstring.
230 | no-docstring-rgx=^_
231 |
232 | # List of decorators that produce properties, such as abc.abstractproperty. Add
233 | # to this list to register other decorators that produce valid properties.
234 | # These decorators are taken in consideration only for invalid-name.
235 | property-classes=abc.abstractproperty
236 |
237 | # Regular expression matching correct type alias names. If left empty, type
238 | # alias names will be checked with the set naming style.
239 | #typealias-rgx=
240 |
241 | # Regular expression matching correct type variable names. If left empty, type
242 | # variable names will be checked with the set naming style.
243 | #typevar-rgx=
244 |
245 | # Naming style matching correct variable names.
246 | variable-naming-style=snake_case
247 |
248 | # Regular expression matching correct variable names. Overrides variable-
249 | # naming-style. If left empty, variable names will be checked with the set
250 | # naming style.
251 | #variable-rgx=
252 |
253 |
254 | [CLASSES]
255 |
256 | # Warn about protected attribute access inside special methods
257 | check-protected-access-in-special-methods=no
258 |
259 | # List of method names used to declare (i.e. assign) instance attributes.
260 | defining-attr-methods=__init__,
261 | __new__,
262 | setUp,
263 | asyncSetUp,
264 | __post_init__
265 |
266 | # List of member names, which should be excluded from the protected access
267 | # warning.
268 | exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit
269 |
270 | # List of valid names for the first argument in a class method.
271 | valid-classmethod-first-arg=cls
272 |
273 | # List of valid names for the first argument in a metaclass class method.
274 | valid-metaclass-classmethod-first-arg=mcs
275 |
276 |
277 | [DESIGN]
278 |
279 | # List of regular expressions of class ancestor names to ignore when counting
280 | # public methods (see R0903)
281 | exclude-too-few-public-methods=
282 |
283 | # List of qualified class names to ignore when counting class parents (see
284 | # R0901)
285 | ignored-parents=
286 |
287 | # Maximum number of arguments for function / method.
288 | max-args=5
289 |
290 | # Maximum number of attributes for a class (see R0902).
291 | max-attributes=7
292 |
293 | # Maximum number of boolean expressions in an if statement (see R0916).
294 | max-bool-expr=5
295 |
296 | # Maximum number of branch for function / method body.
297 | max-branches=12
298 |
299 | # Maximum number of locals for function / method body.
300 | max-locals=20
301 |
302 | # Maximum number of parents for a class (see R0901).
303 | max-parents=7
304 |
305 | # Maximum number of public methods for a class (see R0904).
306 | max-public-methods=20
307 |
308 | # Maximum number of return / yield for function / method body.
309 | max-returns=6
310 |
311 | # Maximum number of statements in function / method body.
312 | max-statements=50
313 |
314 | # Minimum number of public methods for a class (see R0903).
315 | min-public-methods=2
316 |
317 |
318 | [EXCEPTIONS]
319 |
320 | # Exceptions that will emit a warning when caught.
321 | overgeneral-exceptions=builtins.BaseException,builtins.Exception
322 |
323 |
324 | [FORMAT]
325 |
326 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
327 | expected-line-ending-format=
328 |
329 | # Regexp for a line that is allowed to be longer than the limit.
330 | ignore-long-lines=^\s*(# )??$
331 |
332 | # Number of spaces of indent required inside a hanging or continued line.
333 | indent-after-paren=4
334 |
335 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
336 | # tab).
337 | indent-string=' '
338 |
339 | # Maximum number of characters on a single line.
340 | max-line-length=300
341 |
342 | # Maximum number of lines in a module.
343 | max-module-lines=1000
344 |
345 | # Allow the body of a class to be on the same line as the declaration if body
346 | # contains single statement.
347 | single-line-class-stmt=no
348 |
349 | # Allow the body of an if to be on the same line as the test if there is no
350 | # else.
351 | single-line-if-stmt=no
352 |
353 |
354 | [IMPORTS]
355 |
356 | # List of modules that can be imported at any level, not just the top level
357 | # one.
358 | allow-any-import-level=
359 |
360 | # Allow explicit reexports by alias from a package __init__.
361 | allow-reexport-from-package=no
362 |
363 | # Allow wildcard imports from modules that define __all__.
364 | allow-wildcard-with-all=no
365 |
366 | # Deprecated modules which should not be used, separated by a comma.
367 | deprecated-modules=
368 |
369 | # Output a graph (.gv or any supported image format) of external dependencies
370 | # to the given file (report RP0402 must not be disabled).
371 | ext-import-graph=
372 |
373 | # Output a graph (.gv or any supported image format) of all (i.e. internal and
374 | # external) dependencies to the given file (report RP0402 must not be
375 | # disabled).
376 | import-graph=
377 |
378 | # Output a graph (.gv or any supported image format) of internal dependencies
379 | # to the given file (report RP0402 must not be disabled).
380 | int-import-graph=
381 |
382 | # Force import order to recognize a module as part of the standard
383 | # compatibility libraries.
384 | known-standard-library=
385 |
386 | # Force import order to recognize a module as part of a third party library.
387 | known-third-party=enchant
388 |
389 | # Couples of modules and preferred modules, separated by a comma.
390 | preferred-modules=
391 |
392 |
393 | [LOGGING]
394 |
395 | # The type of string formatting that logging methods do. `old` means using %
396 | # formatting, `new` is for `{}` formatting.
397 | logging-format-style=old
398 |
399 | # Logging modules to check that the string format arguments are in logging
400 | # function parameter format.
401 | logging-modules=logging
402 |
403 |
404 | [MESSAGES CONTROL]
405 |
406 | # Only show warnings with the listed confidence levels. Leave empty to show
407 | # all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE,
408 | # UNDEFINED.
409 | confidence=HIGH,
410 | CONTROL_FLOW,
411 | INFERENCE,
412 | INFERENCE_FAILURE,
413 | UNDEFINED
414 |
415 | # Disable the message, report, category or checker with the given id(s). You
416 | # can either give multiple identifiers separated by comma (,) or put this
417 | # option multiple times (only on the command line, not in the configuration
418 | # file where it should appear only once). You can also use "--disable=all" to
419 | # disable everything first and then re-enable specific checks. For example, if
420 | # you want to run only the similarities checker, you can use "--disable=all
421 | # --enable=similarities". If you want to run only the classes checker, but have
422 | # no Warning level messages displayed, use "--disable=all --enable=classes
423 | # --disable=W".
424 | disable=raw-checker-failed,
425 | bad-inline-option,
426 | locally-disabled,
427 | file-ignored,
428 | suppressed-message,
429 | useless-suppression,
430 | deprecated-pragma,
431 | use-symbolic-message-instead,
432 | E0015,
433 | W0123,
434 | W0105
435 |
436 | # Enable the message, report, category or checker with the given id(s). You can
437 | # either give multiple identifier separated by comma (,) or put this option
438 | # multiple time (only on the command line, not in the configuration file where
439 | # it should appear only once). See also the "--disable" option for examples.
440 | enable=c-extension-no-member
441 |
442 |
443 | [METHOD_ARGS]
444 |
445 | # List of qualified names (i.e., library.method) which require a timeout
446 | # parameter e.g. 'requests.api.get,requests.api.post'
447 | timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request
448 |
449 |
450 | [MISCELLANEOUS]
451 |
452 | # List of note tags to take in consideration, separated by a comma.
453 | notes=FIXME,
454 | XXX,
455 | TODO
456 |
457 | # Regular expression of note tags to take in consideration.
458 | notes-rgx=
459 |
460 |
461 | [REFACTORING]
462 |
463 | # Maximum number of nested blocks for function / method body
464 | max-nested-blocks=5
465 |
466 | # Complete name of functions that never returns. When checking for
467 | # inconsistent-return-statements if a never returning function is called then
468 | # it will be considered as an explicit return statement and no message will be
469 | # printed.
470 | never-returning-functions=sys.exit,argparse.parse_error
471 |
472 |
473 | [REPORTS]
474 |
475 | # Python expression which should return a score less than or equal to 10. You
476 | # have access to the variables 'fatal', 'error', 'warning', 'refactor',
477 | # 'convention', and 'info' which contain the number of messages in each
478 | # category, as well as 'statement' which is the total number of statements
479 | # analyzed. This score is used by the global evaluation report (RP0004).
480 | evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))
481 |
482 | # Template used to display messages. This is a python new-style format string
483 | # used to format the message information. See doc for all details.
484 | msg-template=
485 |
486 | # Set the output format. Available formats are text, parseable, colorized, json
487 | # and msvs (visual studio). You can also give a reporter class, e.g.
488 | # mypackage.mymodule.MyReporterClass.
489 | #output-format=
490 |
491 | # Tells whether to display a full report or only the messages.
492 | reports=no
493 |
494 | # Activate the evaluation score.
495 | score=yes
496 |
497 |
498 | [SIMILARITIES]
499 |
500 | # Comments are removed from the similarity computation
501 | ignore-comments=yes
502 |
503 | # Docstrings are removed from the similarity computation
504 | ignore-docstrings=yes
505 |
506 | # Imports are removed from the similarity computation
507 | ignore-imports=yes
508 |
509 | # Signatures are removed from the similarity computation
510 | ignore-signatures=yes
511 |
512 | # Minimum lines number of a similarity.
513 | min-similarity-lines=4
514 |
515 |
516 | [SPELLING]
517 |
518 | # Limits count of emitted suggestions for spelling mistakes.
519 | max-spelling-suggestions=4
520 |
521 | # Spelling dictionary name. No available dictionaries : You need to install
522 | # both the python package and the system dependency for enchant to work..
523 | spelling-dict=
524 |
525 | # List of comma separated words that should be considered directives if they
526 | # appear at the beginning of a comment and should not be checked.
527 | spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
528 |
529 | # List of comma separated words that should not be checked.
530 | spelling-ignore-words=
531 |
532 | # A path to a file that contains the private dictionary; one word per line.
533 | spelling-private-dict-file=
534 |
535 | # Tells whether to store unknown words to the private dictionary (see the
536 | # --spelling-private-dict-file option) instead of raising a message.
537 | spelling-store-unknown-words=no
538 |
539 |
540 | [STRING]
541 |
542 | # This flag controls whether inconsistent-quotes generates a warning when the
543 | # character used as a quote delimiter is used inconsistently within a module.
544 | check-quote-consistency=no
545 |
546 | # This flag controls whether the implicit-str-concat should generate a warning
547 | # on implicit string concatenation in sequences defined over several lines.
548 | check-str-concat-over-line-jumps=no
549 |
550 |
551 | [TYPECHECK]
552 |
553 | # List of decorators that produce context managers, such as
554 | # contextlib.contextmanager. Add to this list to register other decorators that
555 | # produce valid context managers.
556 | contextmanager-decorators=contextlib.contextmanager
557 |
558 | # List of members which are set dynamically and missed by pylint inference
559 | # system, and so shouldn't trigger E1101 when accessed. Python regular
560 | # expressions are accepted.
561 | generated-members=
562 |
563 | # Tells whether to warn about missing members when the owner of the attribute
564 | # is inferred to be None.
565 | ignore-none=yes
566 |
567 | # This flag controls whether pylint should warn about no-member and similar
568 | # checks whenever an opaque object is returned when inferring. The inference
569 | # can return multiple potential results while evaluating a Python object, but
570 | # some branches might not be evaluated, which results in partial inference. In
571 | # that case, it might be useful to still emit no-member and other checks for
572 | # the rest of the inferred objects.
573 | ignore-on-opaque-inference=yes
574 |
575 | # List of symbolic message names to ignore for Mixin members.
576 | ignored-checks-for-mixins=no-member,
577 | not-async-context-manager,
578 | not-context-manager,
579 | attribute-defined-outside-init
580 |
581 | # List of class names for which member attributes should not be checked (useful
582 | # for classes with dynamically set attributes). This supports the use of
583 | # qualified names.
584 | ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace
585 |
586 | # Show a hint with possible names when a member name was not found. The aspect
587 | # of finding the hint is based on edit distance.
588 | missing-member-hint=yes
589 |
590 | # The minimum edit distance a name should have in order to be considered a
591 | # similar match for a missing member name.
592 | missing-member-hint-distance=1
593 |
594 | # The total number of similar names that should be taken in consideration when
595 | # showing a hint for a missing member.
596 | missing-member-max-choices=1
597 |
598 | # Regex pattern to define which classes are considered mixins.
599 | mixin-class-rgx=.*[Mm]ixin
600 |
601 | # List of decorators that change the signature of a decorated function.
602 | signature-mutators=
603 |
604 |
605 | [VARIABLES]
606 |
607 | # List of additional names supposed to be defined in builtins. Remember that
608 | # you should avoid defining new builtins when possible.
609 | additional-builtins=
610 |
611 | # Tells whether unused global variables should be treated as a violation.
612 | allow-global-unused-variables=yes
613 |
614 | # List of names allowed to shadow builtins
615 | allowed-redefined-builtins=
616 |
617 | # List of strings which can identify a callback function by name. A callback
618 | # name must start or end with one of those strings.
619 | callbacks=cb_,
620 | _cb
621 |
622 | # A regular expression matching the name of dummy variables (i.e. expected to
623 | # not be used).
624 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
625 |
626 | # Argument names that match this expression will be ignored.
627 | ignored-argument-names=_.*|^ignored_|^unused_
628 |
629 | # Tells whether we should check for unused import in __init__ files.
630 | init-import=no
631 |
632 | # List of qualified module names which can have objects that can redefine
633 | # builtins.
634 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
635 |
--------------------------------------------------------------------------------