tranquillity 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. tranquillity-0.1.0/PKG-INFO +23 -0
  2. tranquillity-0.1.0/README.md +91 -0
  3. tranquillity-0.1.0/ctOS.py +43 -0
  4. tranquillity-0.1.0/docker_launcher.py +37 -0
  5. tranquillity-0.1.0/pyproject.toml +43 -0
  6. tranquillity-0.1.0/settings/settings.py +5 -0
  7. tranquillity-0.1.0/setup.cfg +4 -0
  8. tranquillity-0.1.0/src/core/__init__.py +0 -0
  9. tranquillity-0.1.0/src/core/gemini.py +244 -0
  10. tranquillity-0.1.0/src/ctOS/help.py +21 -0
  11. tranquillity-0.1.0/src/ctOS/light/light.py +15 -0
  12. tranquillity-0.1.0/src/ctOS/light/toggle_light.py +18 -0
  13. tranquillity-0.1.0/src/ctOS/music/get_id_device.py +17 -0
  14. tranquillity-0.1.0/src/ctOS/music/get_token_tmp.py +115 -0
  15. tranquillity-0.1.0/src/ctOS/music/init_token.py +34 -0
  16. tranquillity-0.1.0/src/ctOS/music/music.py +26 -0
  17. tranquillity-0.1.0/src/ctOS/music/pause.py +15 -0
  18. tranquillity-0.1.0/src/ctOS/music/resume.py +15 -0
  19. tranquillity-0.1.0/src/ctOS/start/init.py +125 -0
  20. tranquillity-0.1.0/src/ctOS/start/start.py +61 -0
  21. tranquillity-0.1.0/src/subsystem/autonome_sys/autonome.py +8 -0
  22. tranquillity-0.1.0/src/subsystem/create_event.py +47 -0
  23. tranquillity-0.1.0/src/subsystem/get_creds.py +30 -0
  24. tranquillity-0.1.0/src/subsystem/get_email.py +101 -0
  25. tranquillity-0.1.0/src/subsystem/list_event.py +65 -0
  26. tranquillity-0.1.0/src/subsystem/notification_system.py +17 -0
  27. tranquillity-0.1.0/src/subsystem/search_web.py +48 -0
  28. tranquillity-0.1.0/src/tranquillity.py +10 -0
  29. tranquillity-0.1.0/tranquillity.egg-info/PKG-INFO +23 -0
  30. tranquillity-0.1.0/tranquillity.egg-info/SOURCES.txt +32 -0
  31. tranquillity-0.1.0/tranquillity.egg-info/dependency_links.txt +1 -0
  32. tranquillity-0.1.0/tranquillity.egg-info/entry_points.txt +2 -0
  33. tranquillity-0.1.0/tranquillity.egg-info/requires.txt +21 -0
  34. tranquillity-0.1.0/tranquillity.egg-info/top_level.txt +4 -0
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: tranquillity
3
+ Version: 0.1.0
4
+ Provides-Extra: app
5
+ Requires-Dist: opencv-contrib-python; extra == "app"
6
+ Requires-Dist: requests; extra == "app"
7
+ Requires-Dist: ollama; extra == "app"
8
+ Requires-Dist: piper-tts; extra == "app"
9
+ Requires-Dist: vosk; extra == "app"
10
+ Requires-Dist: rapidfuzz; extra == "app"
11
+ Requires-Dist: sounddevice; extra == "app"
12
+ Requires-Dist: pyyaml; extra == "app"
13
+ Requires-Dist: spacy; extra == "app"
14
+ Requires-Dist: rich; extra == "app"
15
+ Requires-Dist: python-dotenv; extra == "app"
16
+ Requires-Dist: click; extra == "app"
17
+ Requires-Dist: google-generativeai; extra == "app"
18
+ Requires-Dist: google-genai; extra == "app"
19
+ Requires-Dist: google-api-python-client; extra == "app"
20
+ Requires-Dist: google-auth-httplib2; extra == "app"
21
+ Requires-Dist: google-auth-oauthlib; extra == "app"
22
+ Requires-Dist: html2text; extra == "app"
23
+ Requires-Dist: feedparser; extra == "app"
@@ -0,0 +1,91 @@
1
+ # Tranquillity
2
+
3
+ What is Tranquillity?
4
+
5
+ Tranquillity is a project that aims to recreate a personal assistant with GLaDOS AI voice, capable of assisting with any task on a PC.
6
+
7
+ Inspired by the famous AI from the video game Portal 2, as well as projects already created throughout the Internet.
8
+
9
+ <img src="https://github.com/user-attachments/assets/1f69f6fd-e8fd-4431-956d-32d97b91bb56"
10
+ alt="Picture"
11
+ width="800"
12
+ height="600"
13
+ style="display: block; margin: 10" />
14
+
15
+ ***
16
+
17
+ Features:
18
+
19
+ * **Personal AI Assistant** with GLaDOS voice synthesis (TTS).
20
+ * **Voice Recognition** - French language support with rapid command parsing.
21
+ * **Music Control** - Spotify integration (pause, resume, device management).
22
+ * **Lighting Control** - Smart light management.
23
+ * **Discord Integration** - Send messages to Discord directly from commands.
24
+ * **ctOS CLI** - Custom command-line interface created specifically for Tranquillity.
25
+ * **Voice-to-Voice LLM** - Direct voice conversation with AI processing.
26
+
27
+ ***
28
+
29
+ Planned Features:
30
+
31
+ * Enhanced gesture recognition and commands.
32
+ * Extended camera vision capabilities.
33
+ * Additional smart home integrations.
34
+ * More language support.
35
+ * And much more.
36
+
37
+ ***
38
+
39
+ Development / Usage:
40
+
41
+ For Linux or Windows:
42
+
43
+ ```bash
44
+ ./env/bin/python -m pip install -r requirement.txt
45
+ ```
46
+
47
+ For NixOS:
48
+
49
+ Requires devenv
50
+
51
+ ```bash
52
+ devenv init
53
+ ```
54
+
55
+ then
56
+
57
+ ```bash
58
+ devenv start
59
+ ```
60
+
61
+ Usage:
62
+
63
+ ```bash
64
+ python ctOS.py --init
65
+ python ctOS.py --start
66
+ ```
67
+
68
+ Docker:
69
+
70
+ ```bash
71
+ docker build -f docker/Dockerfile -t tranquillity:local .
72
+ TRANQUILLITY_IMAGE=tranquillity:local python docker_launcher.py --help
73
+
74
+ ```
75
+
76
+ The `tranquillity` command from `pyproject.toml` runs `docker_launcher.py` on the host. The Docker image itself runs `ctOS.py` and forwards the arguments supplied to the launcher.
77
+
78
+ ***
79
+
80
+ Inspiration:
81
+
82
+ This project wouldn't exist without inspiration and help.
83
+ The project is primarily educational, and here are the repositories that helped me:
84
+
85
+ * https://github.com/dnhkng/GLaDOS
86
+
87
+ ***
88
+
89
+ I welcome any help!
90
+
91
+
@@ -0,0 +1,43 @@
1
+ from src.ctOS.start.init import demo
2
+ from src.ctOS.start.start import start
3
+ from src.ctOS.help import help
4
+ import sys
5
+ import asyncio
6
+ from src.subsystem.notification_system import send_discord_message
7
+ from dotenv import load_dotenv
8
+ from src.tranquillity import init as tranquillity
9
+ from src.ctOS.music.music import music_command
10
+ from src.ctOS.music.init_token import init
11
+ load_dotenv()
12
+
13
+ def main():
14
+ nb = len(sys.argv)
15
+
16
+ if nb <= 1:
17
+ help()
18
+ return
19
+ if sys.argv[1] == "--demo":
20
+ demo()
21
+ start()
22
+ return
23
+ if sys.argv[1] == "--music":
24
+ music_command()
25
+ return
26
+ if sys.argv[1] == "--send":
27
+ if sys.argv[2] == None:
28
+ return 84
29
+ asyncio.run(send_discord_message(sys.argv[2]))
30
+ return
31
+ if sys.argv[1] == "--init":
32
+ init()
33
+ return
34
+ if sys.argv[1] == "--start":
35
+ tranquillity()
36
+ return
37
+ if sys.argv[1] == "--help" or "-h":
38
+ help()
39
+ else:
40
+ return 84
41
+
42
+ if __name__ == "__main__":
43
+ main()
@@ -0,0 +1,37 @@
1
+ import os
2
+ import shutil
3
+ import subprocess
4
+ import sys
5
+
6
+
7
+ IMAGE_ENV = "TRANQUILLITY_IMAGE"
8
+
9
+
10
+ def main() -> int:
11
+ image = os.environ.get(IMAGE_ENV)
12
+ if not image:
13
+ print(
14
+ f"{IMAGE_ENV} n'est pas defini. Exemple: "
15
+ "export TRANQUILLITY_IMAGE=ghcr.io/mon-compte/tranquillity:latest",
16
+ file=sys.stderr,
17
+ )
18
+ return 2
19
+
20
+ if shutil.which("docker") is None:
21
+ print("Docker est requis mais la commande docker est introuvable.", file=sys.stderr)
22
+ return 127
23
+
24
+ try:
25
+ subprocess.run(["docker", "info"], check=True)
26
+ subprocess.run(["docker", "pull", image], check=True)
27
+ return subprocess.run(
28
+ ["docker", "run", "--rm", "-it", "--name", "tranquillity", image, *sys.argv[1:]]
29
+ ).returncode
30
+ except subprocess.CalledProcessError as error:
31
+ return error.returncode
32
+ except KeyboardInterrupt:
33
+ return 130
34
+
35
+
36
+ if __name__ == "__main__":
37
+ raise SystemExit(main())
@@ -0,0 +1,43 @@
1
+
2
+ [build-system]
3
+ requires = ["setuptools >= 77.0.3"]
4
+ build-backend = "setuptools.build_meta"
5
+
6
+ [project]
7
+ name = "tranquillity"
8
+ version = "0.1.0"
9
+ dependencies = []
10
+
11
+ [project.optional-dependencies]
12
+ app = [
13
+ "opencv-contrib-python",
14
+ "requests",
15
+ "ollama",
16
+ "piper-tts",
17
+ "vosk",
18
+ "rapidfuzz",
19
+ "sounddevice",
20
+ "pyyaml",
21
+ "spacy",
22
+ "rich",
23
+ "python-dotenv",
24
+ "click",
25
+ "google-generativeai",
26
+ "google-genai",
27
+ "google-api-python-client",
28
+ "google-auth-httplib2",
29
+ "google-auth-oauthlib",
30
+ "html2text",
31
+ "feedparser",
32
+ ]
33
+
34
+ [project.scripts]
35
+ tranquillity = "docker_launcher:main"
36
+
37
+ [tool.setuptools]
38
+ py-modules = ["ctOS", "docker_launcher"]
39
+
40
+ [tool.setuptools.packages.find]
41
+ where = ["."]
42
+ include = ["src*", "settings*"]
43
+ namespaces = true
@@ -0,0 +1,5 @@
1
+ IA_NAME = "gemini-3.5-flash-lite"
2
+ VOICE_PATH = "model/tts/glados/fr_FR-glados-medium.onnx"
3
+ MODEL_PATH = "model/vosk/vosk-model-small-fr/vosk-model-fr-0.22"
4
+ SAMPLE_RATE = 16000
5
+ BLOCK_SIZE = 4000
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,244 @@
1
+ import os
2
+ import sys
3
+ import sounddevice as sd
4
+ import queue
5
+ import subprocess
6
+ import tempfile
7
+ import wave
8
+ import threading
9
+ import shlex
10
+ from piper import PiperVoice
11
+ from settings.settings import IA_NAME, VOICE_PATH, MODEL_PATH, SAMPLE_RATE, BLOCK_SIZE
12
+ from dotenv import load_dotenv
13
+ import re
14
+ from vosk import Model, KaldiRecognizer, SetLogLevel
15
+ import json
16
+ import requests
17
+ from src.subsystem.search_web import search_web
18
+ from src.subsystem.get_email import get_email
19
+ from google import genai
20
+ from google.genai import types
21
+ from src.subsystem.create_event import create_event
22
+ from src.subsystem.list_event import list_calendar
23
+
24
+ load_dotenv()
25
+
26
+ recording_enabled = True
27
+ audio_queue = queue.Queue()
28
+
29
+ def callback(indata, frames, time, status):
30
+ global recording_enabled
31
+ if status:
32
+ print(status, file=sys.stderr)
33
+ if recording_enabled:
34
+ audio_queue.put(bytes(indata))
35
+
36
+
37
+ def clean_for_tts(text: str) -> str:
38
+ text = re.sub(r'\*\*(.+?)\*\*', r'\1', text)
39
+ text = re.sub(r'\*(.+?)\*', r'\1', text)
40
+ text = re.sub(r'[*#`_]', '', text)
41
+ return text
42
+
43
+ def player_worker(q: "queue.Queue[str | None]"):
44
+ global recording_enabled
45
+ while True:
46
+ path = q.get()
47
+ if path is None:
48
+ return
49
+ recording_enabled = False
50
+ subprocess.run(["mpv", "--no-terminal", "--really-quiet", path])
51
+ recording_enabled = True
52
+ try:
53
+ os.remove(path)
54
+ except OSError:
55
+ pass
56
+
57
+
58
+ def clear_screen():
59
+ os.system('cls' if os.name == 'nt' else 'clear')
60
+
61
+ def get_api_key():
62
+ api_key = os.getenv("GEMINI_API_KEY")
63
+ if api_key:
64
+ return api_key
65
+
66
+ print("=== Configuration de la clé API Gemini ===")
67
+ print("Vous pouvez également définir la variable d'environnement GEMINI_API_KEY pour éviter cette étape.\n")
68
+ while True:
69
+ api_key = input("Veuillez entrer votre clé API Google Gemini : ").strip()
70
+ if api_key:
71
+ return api_key
72
+ print("La clé ne peut pas être vide. Veuillez réessayer.")
73
+
74
+
75
+ def tts_worker(text_q, out_q, voice):
76
+ while True:
77
+ fragment = text_q.get()
78
+ if fragment is None:
79
+ out_q.put(None)
80
+ return
81
+ fd, path = tempfile.mkstemp(prefix="tts_", suffix=".wav")
82
+ os.close(fd)
83
+ with wave.open(path, "wb") as wav_file:
84
+ voice.synthesize_wav(fragment, wav_file)
85
+ out_q.put(path)
86
+
87
+ def detect_command(response_text):
88
+ resultat = re.search(r"(?mi)^command:\s*(.+)$", response_text)
89
+
90
+ if resultat:
91
+ commande = resultat.group(1).strip()
92
+ return resultat.start(), commande
93
+ return None
94
+
95
+ def execute_command(commande_raw):
96
+ if commande_raw:
97
+ try:
98
+ try:
99
+ args = shlex.split(commande_raw)
100
+ subprocess.run(args, shell=False)
101
+ except ValueError:
102
+ subprocess.run(commande_raw, shell=True)
103
+ except Exception as e:
104
+ pass
105
+
106
+ def gemini():
107
+ clear_screen()
108
+ print("Initialisation Tranquillity")
109
+
110
+ api_key = get_api_key()
111
+ model_name = IA_NAME
112
+
113
+ print("Initialisation de la personalité")
114
+
115
+ with open("settings/personality.txt", "r", encoding="utf-8") as f:
116
+ content = f.read()
117
+ system_instruction = content
118
+ if not system_instruction:
119
+ system_instruction = "Tu es un assistant virtuel utile, amical et précis. Réponds toujours en français."
120
+ print(f"-> Contexte par défaut appliqué : \"{system_instruction}\"")
121
+ else:
122
+ print(f"-> Contexte personnalisé appliqué avec succès !")
123
+
124
+ print("\nInitialisation du modèle...")
125
+
126
+ client = genai.Client(api_key=api_key)
127
+
128
+ try:
129
+ chat = client.chats.create(
130
+ model=model_name,
131
+ config=types.GenerateContentConfig(
132
+ system_instruction=system_instruction,
133
+ tools=[search_web, get_email, list_calendar, create_event]
134
+ ),
135
+ history=[]
136
+ )
137
+ except Exception as e:
138
+ print(f"Erreur lors de l'initialisation du modèle : {e}")
139
+ sys.exit(1)
140
+
141
+ voice = PiperVoice.load(VOICE_PATH)
142
+ print("voice init success")
143
+
144
+ print("Initialisation VOSK")
145
+ print("VOSK load")
146
+ SetLogLevel(-1)
147
+
148
+ model = Model(MODEL_PATH)
149
+ rec = KaldiRecognizer(model, SAMPLE_RATE)
150
+
151
+ clear_screen()
152
+ print("==================================================")
153
+ print(" 💬 CHAT INITIALISÉ AVEC SUCCÈS ")
154
+ print("==================================================")
155
+ print(f"Modèle utilisé : {model_name}")
156
+ print("--------------------------------------------------")
157
+ print("Instructions :")
158
+ print(" - Tapez votre message et appuyez sur Entrée.")
159
+ print(" - Écrivez 'exit', 'quit' ou 'quitter' pour fermer le chat.")
160
+ print(" - Écrivez 'clear' pour vider l'écran du terminal.")
161
+ print("==================================================\n")
162
+
163
+ audio_q: "queue.Queue[str | None]" = queue.Queue()
164
+ tts_text_q: "queue.Queue[str | None]" = queue.Queue()
165
+
166
+ t_tts = threading.Thread(target=tts_worker, args=(tts_text_q, audio_q, voice), daemon=True)
167
+ t_tts.start()
168
+ t = threading.Thread(target=player_worker, args=(audio_q,), daemon=True)
169
+ t.start()
170
+
171
+ one_use = False
172
+ print("En écoute...")
173
+ with sd.RawInputStream(
174
+ samplerate=SAMPLE_RATE,
175
+ blocksize=BLOCK_SIZE,
176
+ dtype="int16",
177
+ channels=1,
178
+ callback=callback,
179
+ ):
180
+ while True:
181
+ try:
182
+ data = audio_queue.get()
183
+ audio_q: "queue.Queue[str | None]" = queue.Queue()
184
+ if rec.AcceptWaveform(data) and one_use == False:
185
+ one_use = True
186
+ result = json.loads(rec.Result())
187
+ user_input = result.get("text", "").strip()
188
+ if not user_input:
189
+ one_use = False
190
+ continue
191
+ if user_input.lower() in ['exit', 'quit', 'quitter']:
192
+ print("\n Assistant : Au revoir ! Passez une excellente journée.")
193
+ break
194
+
195
+ if user_input.lower() == 'clear':
196
+ clear_screen()
197
+ print("==================================================")
198
+ print(" CONVERSATION EN COURS ")
199
+ print("================================================--\n")
200
+ one_use = False
201
+ continue
202
+
203
+ print("Vous", user_input)
204
+ print(" Assistant : ", end="", flush=True)
205
+ response = chat.send_message(user_input)
206
+
207
+ streamed_text = response.text
208
+ print(streamed_text, flush=True)
209
+
210
+ command_start = None
211
+ command_match = detect_command(streamed_text)
212
+ if command_match:
213
+ command_start, _ = command_match
214
+ spoken_text = streamed_text[:command_start]
215
+ else:
216
+ spoken_text = streamed_text
217
+
218
+ clean_tts = clean_for_tts(spoken_text)
219
+ if clean_tts.strip():
220
+ tts_text_q.put(clean_tts)
221
+
222
+ print("\n")
223
+
224
+ if command_start is not None:
225
+ _, commande = detect_command(streamed_text)
226
+ if commande:
227
+ print(f"Commande détectée : {commande}")
228
+ run_command = threading.Thread(target=execute_command, args=(commande,), daemon=True)
229
+ run_command.start()
230
+ one_use = False
231
+
232
+ except KeyboardInterrupt:
233
+ print("\n\n Assistant : Chat interrompu. Au revoir !")
234
+ audio_q.put(None)
235
+ t.join()
236
+ break
237
+ except Exception as e:
238
+ print(f"\nUne erreur est survenue lors de la communication avec l'API : {e}\n")
239
+ audio_q.put(None)
240
+ t.join()
241
+ break
242
+
243
+ if __name__ == "__main__":
244
+ gemini()
@@ -0,0 +1,21 @@
1
+ def help():
2
+ print("==================================================")
3
+ print(" Bienvenue dans ctOS - CLI Tranquillity")
4
+ print("==================================================\n")
5
+ print("USAGE :")
6
+ print(" ctOS [commande]")
7
+ print(" ctOS [option]\n")
8
+ print("COMMANDES PRINCIPALES :\n")
9
+ print(" --start Lance le programme principal Tranquillity")
10
+ print("GESTION MUSICALE :\n")
11
+ print(" --music pause Met en pause la musique en cours")
12
+ print(" --music resume Reprend la musique en pause")
13
+ print(" --music device Affiche et gere les appareils audio connectes\n")
14
+ print("CONTROLE DOMOTIQUE :\n")
15
+ print(" --light toggle Active/desactive les lumieres connectees\n")
16
+ print("OUTILS ET CONFIGURATION :\n")
17
+ print(" --init Initialise le token Spotify et vérification")
18
+ print(" --send [message] Envoie un message via Discord\n")
19
+ print("AUTRES :\n")
20
+ print(" --help, -h Affiche ce message d'aide\n")
21
+ print("==================================================\n")
@@ -0,0 +1,15 @@
1
+ import os
2
+ import sys
3
+ from src.ctOS.light.toggle_light import toggle_light
4
+
5
+ def light_command():
6
+ nb = len(sys.argv)
7
+
8
+ if nb <= 2:
9
+ print("no argument give. Please give word")
10
+ return
11
+ if sys.argv[2] == "toggle":
12
+ toggle_light()
13
+ return
14
+ else:
15
+ print("no argument give. Please give word")
@@ -0,0 +1,18 @@
1
+ import os
2
+ import subprocess
3
+
4
+ FIFO_PATH = "/tmp/kitty_shell_input"
5
+ CMD_LIGHT = "./ecc-api --toggle"
6
+
7
+ def toggle_light():
8
+ if not os.path.exists(FIFO_PATH):
9
+ print(f"Sub-Agent: FIFO introuvable: {FIFO_PATH}")
10
+ return
11
+ with open(FIFO_PATH, "w") as fifo:
12
+ fifo.write(CMD_LIGHT + "\n")
13
+ subprocess.run(CMD_LIGHT)
14
+ return
15
+
16
+ def toggle_light():
17
+ subprocess.run(CMD_LIGHT)
18
+ return
@@ -0,0 +1,17 @@
1
+ import requests
2
+ import json
3
+ import sys
4
+ from src.ctOS.music.get_token_tmp import get_token
5
+ import requests
6
+
7
+ def get_id_device():
8
+ access_token = get_token()
9
+ url = "https://api.spotify.com/v1/me/player/devices"
10
+ header = {"Authorization": "Bearer " + access_token}
11
+
12
+ result = requests.get(url, headers=header)
13
+ try:
14
+ body = result.json()
15
+ except Exception:
16
+ body = result.text
17
+ print(body)
@@ -0,0 +1,115 @@
1
+ import os
2
+ import json
3
+ import base64
4
+ import webbrowser
5
+ from http.server import HTTPServer, BaseHTTPRequestHandler
6
+ from urllib.parse import urlencode, urlparse, parse_qs
7
+ from requests import post
8
+ from dotenv import load_dotenv
9
+
10
+ load_dotenv()
11
+
12
+ CLIENT_ID = os.getenv("TOKEN_PUBLIC")
13
+ CLIENT_SECRET = os.getenv("TOKEN_PRIVATE")
14
+ REDIRECT_URI = "http://127.0.0.1:8888/callback"
15
+ SCOPES = "user-read-playback-state user-modify-playback-state"
16
+ TOKEN_FILE = os.path.join(os.path.dirname(__file__), ".spotify_token.json")
17
+
18
+
19
+ def _basic_auth_header():
20
+ auth_string = f"{CLIENT_ID}:{CLIENT_SECRET}".encode("utf-8")
21
+ return "Basic " + base64.b64encode(auth_string).decode("utf-8")
22
+
23
+
24
+ class _CallbackHandler(BaseHTTPRequestHandler):
25
+ code = None
26
+
27
+ def do_GET(self):
28
+ query = parse_qs(urlparse(self.path).query)
29
+ _CallbackHandler.code = query.get("code", [None])[0]
30
+ self.send_response(200)
31
+ self.send_header("Content-type", "text/html")
32
+ self.end_headers()
33
+ self.wfile.write(b"<h1>You can close this.</h1>")
34
+
35
+ def log_message(self, format, *args):
36
+ pass
37
+
38
+
39
+ def _request_authorization_code():
40
+ params = {
41
+ "client_id": CLIENT_ID,
42
+ "response_type": "code",
43
+ "redirect_uri": REDIRECT_URI,
44
+ "scope": SCOPES,
45
+ }
46
+ auth_url = "https://accounts.spotify.com/authorize?" + urlencode(params)
47
+ webbrowser.open(auth_url)
48
+
49
+ server = HTTPServer(("127.0.0.1", 8888), _CallbackHandler)
50
+ server.handle_request()
51
+ return _CallbackHandler.code
52
+
53
+
54
+ def _exchange_code_for_tokens(code):
55
+ url = "https://accounts.spotify.com/api/token"
56
+ headers = {
57
+ "Authorization": _basic_auth_header(),
58
+ "Content-Type": "application/x-www-form-urlencoded",
59
+ }
60
+ data = {
61
+ "grant_type": "authorization_code",
62
+ "code": code,
63
+ "redirect_uri": REDIRECT_URI,
64
+ }
65
+ result = post(url, headers=headers, data=data)
66
+ result.raise_for_status()
67
+ return result.json()
68
+
69
+
70
+ def _refresh_access_token(refresh_token):
71
+ url = "https://accounts.spotify.com/api/token"
72
+ headers = {
73
+ "Authorization": _basic_auth_header(),
74
+ "Content-Type": "application/x-www-form-urlencoded",
75
+ }
76
+ data = {
77
+ "grant_type": "refresh_token",
78
+ "refresh_token": refresh_token,
79
+ }
80
+ result = post(url, headers=headers, data=data)
81
+ result.raise_for_status()
82
+ return result.json()
83
+
84
+
85
+ def _save_tokens(tokens):
86
+ with open(TOKEN_FILE, "w") as f:
87
+ json.dump(tokens, f)
88
+
89
+
90
+ def _load_tokens():
91
+ if not os.path.exists(TOKEN_FILE):
92
+ return None
93
+ with open(TOKEN_FILE) as f:
94
+ return json.load(f)
95
+
96
+
97
+ def get_token():
98
+ if not CLIENT_ID or not CLIENT_SECRET:
99
+ raise RuntimeError("TOKEN_PUBLIC et TOKEN_PRIVATE doivent etre definis dans l'environnement")
100
+
101
+ tokens = _load_tokens()
102
+
103
+ if tokens and "refresh_token" in tokens:
104
+ new_tokens = _refresh_access_token(tokens["refresh_token"])
105
+ new_tokens.setdefault("refresh_token", tokens["refresh_token"])
106
+ _save_tokens(new_tokens)
107
+ return new_tokens["access_token"]
108
+
109
+ code = _request_authorization_code()
110
+ if not code:
111
+ raise RuntimeError("Aucun code recu, autorisation echouee")
112
+
113
+ tokens = _exchange_code_for_tokens(code)
114
+ _save_tokens(tokens)
115
+ return tokens["access_token"]