spothomelight 0.1.0__py3-none-any.whl

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.
File without changes
spothomelight/auth.py ADDED
@@ -0,0 +1,47 @@
1
+ import spotipy
2
+ from spotipy.oauth2 import SpotifyOAuth
3
+ import logging
4
+ from .config import TOKEN_FILE
5
+
6
+ def get_spotify_client(config):
7
+ client_id = config['SPOTIFY']['client_id']
8
+ client_secret = config['SPOTIFY']['client_secret']
9
+ redirect_uri = config['SPOTIFY']['redirect_uri']
10
+
11
+ if not client_id or not client_secret:
12
+ print("错误: 请先在配置文件中填写 Client ID 和 Client Secret。")
13
+ print("运行 'spothomelight -c' 进行配置。")
14
+ return None
15
+
16
+ scope = "user-read-playback-state"
17
+
18
+ sp_oauth = SpotifyOAuth(
19
+ client_id=client_id,
20
+ client_secret=client_secret,
21
+ redirect_uri=redirect_uri,
22
+ scope=scope,
23
+ open_browser=False,
24
+ cache_path=TOKEN_FILE
25
+ )
26
+
27
+ token_info = sp_oauth.get_cached_token()
28
+
29
+ if not token_info:
30
+ auth_url = sp_oauth.get_authorize_url()
31
+ print("初次运行需要登录 Spotify。")
32
+ print("考虑到服务器环境,请复制以下 URL 到你电脑的浏览器中打开:")
33
+ print(f"\n{auth_url}\n")
34
+ print("登录并授权后,浏览器会重定向到 127.0.0.1。")
35
+ print("请复制地址栏中的完整 URL,并粘贴到下面。")
36
+
37
+ response = input("粘贴 URL: ").strip()
38
+
39
+ try:
40
+ code = sp_oauth.parse_response_code(response)
41
+ token_info = sp_oauth.get_access_token(code)
42
+ print(f"登录成功。Token 已保存至: {TOKEN_FILE}")
43
+ except Exception as e:
44
+ print(f"登录失败: {e}")
45
+ return None
46
+
47
+ return spotipy.Spotify(auth=token_info['access_token'], oauth_manager=sp_oauth)
@@ -0,0 +1,87 @@
1
+ import os
2
+ import sys
3
+ import configparser
4
+ import subprocess
5
+ import platform
6
+ from appdirs import user_config_dir
7
+
8
+ APP_NAME = "spothomelight"
9
+ CONFIG_DIR = user_config_dir(APP_NAME)
10
+ CONFIG_FILE = os.path.join(CONFIG_DIR, "spothomelight.conf")
11
+ TOKEN_FILE = os.path.join(CONFIG_DIR, "token.json")
12
+
13
+ DEFAULT_CONFIG = """[SPOTIFY]
14
+ client_id =
15
+ client_secret =
16
+ redirect_uri = http://127.0.0.1:29092/callback
17
+
18
+ [HOME_ASSISTANT]
19
+ ha_url = http://127.0.0.1:8123
20
+ webhook_id =
21
+
22
+ [GENERAL]
23
+ interval = 5
24
+ """
25
+
26
+ HA_YAML_TEMPLATE = """
27
+ 配置完毕后,请配置 Home Assistant 的自动化配置。
28
+
29
+ alias: Spotify Cover Sync
30
+ description: ""
31
+ mode: restart
32
+ trigger:
33
+ - platform: webhook
34
+ webhook_id: ""
35
+ local_only: true
36
+ condition: []
37
+ action:
38
+ - service: light.turn_on
39
+ target:
40
+ entity_id: light.pending
41
+ data:
42
+ rgb_color: "{{ trigger.json.rgb }}"
43
+ brightness_pct: 100
44
+ transition: 2
45
+ """
46
+
47
+ def ensure_config():
48
+ if not os.path.exists(CONFIG_DIR):
49
+ os.makedirs(CONFIG_DIR)
50
+ if not os.path.exists(CONFIG_FILE):
51
+ with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
52
+ f.write(DEFAULT_CONFIG)
53
+ print(f"配置文件已生成: {CONFIG_FILE}")
54
+
55
+ def load_config():
56
+ ensure_config()
57
+ config = configparser.ConfigParser()
58
+ config.read(CONFIG_FILE)
59
+ return config
60
+
61
+ def open_config_editor():
62
+ ensure_config()
63
+
64
+ print(f"\n正在打开配置文件: {CONFIG_FILE}")
65
+ print("请填写 Spotify Client ID/Secret 和 Home Assistant Webhook ID。")
66
+ print(HA_YAML_TEMPLATE)
67
+
68
+ system_platform = platform.system()
69
+
70
+ if system_platform == "Windows":
71
+ try:
72
+ os.startfile(CONFIG_FILE)
73
+ except Exception as e:
74
+ print(f"无法自动打开编辑器: {e}")
75
+ print("请手动打开修改。")
76
+ else:
77
+ editor = os.environ.get('EDITOR')
78
+ if not editor:
79
+ if os.path.exists("/usr/bin/nano"):
80
+ editor = "nano"
81
+ elif os.path.exists("/usr/bin/vi"):
82
+ editor = "vi"
83
+ else:
84
+ print("未找到默认编辑器,请手动编辑。")
85
+ return
86
+
87
+ subprocess.call([editor, CONFIG_FILE])
spothomelight/core.py ADDED
@@ -0,0 +1,83 @@
1
+ import time
2
+ import requests
3
+ import json
4
+ from .utils import get_image_color, write_pid
5
+ from .auth import get_spotify_client
6
+
7
+ def run_loop(config):
8
+ write_pid()
9
+
10
+ sp = get_spotify_client(config)
11
+ if not sp:
12
+ return
13
+
14
+ ha_url = config['HOME_ASSISTANT']['ha_url'].rstrip('/')
15
+ webhook_id = config['HOME_ASSISTANT']['webhook_id']
16
+ interval = int(config['GENERAL']['interval'])
17
+
18
+ if not webhook_id:
19
+ print("错误: 配置文件中 webhook_id 为空,无法发送数据。")
20
+ return
21
+
22
+ webhook_full_url = f"{ha_url}/api/webhook/{webhook_id}"
23
+
24
+ print(f"服务已启动。监控频率: {interval}秒")
25
+ print(f"Home Assistant 地址: {webhook_full_url}")
26
+
27
+ last_track_id = None
28
+ last_is_playing = False
29
+
30
+ while True:
31
+ try:
32
+ playback = sp.current_playback()
33
+
34
+ if playback and playback['is_playing']:
35
+ item = playback['item']
36
+ if not item:
37
+ time.sleep(interval)
38
+ continue
39
+
40
+ track_id = item['id']
41
+
42
+ if track_id != last_track_id or not last_is_playing:
43
+ print(f"检测到正在播放: {item['name']} - {item['artists'][0]['name']}")
44
+
45
+ images = item['album']['images']
46
+ image_url = images[0]['url'] if images else None
47
+
48
+ if image_url:
49
+ rgb = get_image_color(image_url)
50
+ if rgb:
51
+ payload = {
52
+ "state": "playing",
53
+ "title": item['name'],
54
+ "artist": item['artists'][0]['name'],
55
+ "image_url": image_url,
56
+ "rgb": list(rgb),
57
+ "hex": '#%02x%02x%02x' % rgb
58
+ }
59
+
60
+ try:
61
+ requests.post(webhook_full_url, json=payload, timeout=5)
62
+ print(f"已发送颜色 {payload['hex']} 到 HA")
63
+ except Exception as e:
64
+ print(f"发送 Webhook 失败: {e}")
65
+
66
+ last_track_id = track_id
67
+
68
+ last_is_playing = True
69
+
70
+ else:
71
+ if last_is_playing:
72
+ print("播放已暂停/停止")
73
+ # try:
74
+ # requests.post(webhook_full_url, json={"state": "paused"}, timeout=5)
75
+ # except:
76
+ # pass
77
+ last_is_playing = False
78
+
79
+ except Exception as e:
80
+ print(f"运行循环发生错误: {e}")
81
+ time.sleep(10)
82
+
83
+ time.sleep(interval)
spothomelight/main.py ADDED
@@ -0,0 +1,48 @@
1
+ import argparse
2
+ import sys
3
+ from .config import load_config, open_config_editor
4
+ from .service import setup_autostart
5
+ from .utils import check_running, stop_process
6
+ from .core import run_loop
7
+
8
+ def main():
9
+ parser = argparse.ArgumentParser(
10
+ prog="SpotHomeLight",
11
+ description="Spotify 专辑封面颜色控制智能家居(Home Assistant)。",
12
+ formatter_class=argparse.RawTextHelpFormatter,
13
+ epilog="""
14
+ """
15
+ )
16
+
17
+ parser.add_argument("-c", "--config", action="store_true", help="打开配置文件")
18
+ parser.add_argument("-a", "--autostart", action="store_true", help="配置开机自动运行")
19
+ parser.add_argument("-s", "--stop", action="store_true", help="停止正在运行的服务")
20
+
21
+ args = parser.parse_args()
22
+
23
+ if args.config:
24
+ open_config_editor()
25
+ sys.exit(0)
26
+
27
+ if args.stop:
28
+ stop_process()
29
+ sys.exit(0)
30
+
31
+ if args.autostart:
32
+ setup_autostart()
33
+ sys.exit(0)
34
+
35
+ pid = check_running()
36
+ if pid:
37
+ print(f"错误: 服务已在运行中 (PID: {pid})。请先停止或直接使用。")
38
+ sys.exit(1)
39
+
40
+ config = load_config()
41
+ try:
42
+ run_loop(config)
43
+ except KeyboardInterrupt:
44
+ print("\n用户手动停止。")
45
+ stop_process()
46
+
47
+ if __name__ == "__main__":
48
+ main()
@@ -0,0 +1,52 @@
1
+ import os
2
+ import sys
3
+ import platform
4
+ import subprocess
5
+
6
+ def setup_autostart():
7
+ system = platform.system()
8
+ python_exec = sys.executable
9
+ script_cmd = f'"{python_exec}" -m spothomelight.main'
10
+
11
+ if system == "Linux":
12
+ service_dir = os.path.expanduser("~/.config/systemd/user")
13
+ if not os.path.exists(service_dir):
14
+ os.makedirs(service_dir)
15
+
16
+ service_content = f"""[Unit]
17
+ Description=SpotHomeLight Service
18
+ After=network.target
19
+
20
+ [Service]
21
+ ExecStart={script_cmd}
22
+ Restart=always
23
+ RestartSec=10
24
+
25
+ [Install]
26
+ WantedBy=default.target
27
+ """
28
+ service_path = os.path.join(service_dir, "spothomelight.service")
29
+ with open(service_path, "w") as f:
30
+ f.write(service_content)
31
+
32
+ print(f"已生成 Systemd 服务文件: {service_path}")
33
+ print("正在启用服务...")
34
+ os.system("systemctl --user daemon-reload")
35
+ os.system("systemctl --user enable --now spothomelight")
36
+ print("完成。请使用 'systemctl --user status spothomelight' 查看状态。")
37
+
38
+ elif system == "Windows":
39
+ task_name = "SpotHomeLightAutoStart"
40
+ cmd = [
41
+ "schtasks", "/Create", "/F",
42
+ "/TN", task_name,
43
+ "/TR", script_cmd,
44
+ "/SC", "ONLOGON",
45
+ "/RL", "HIGHEST"
46
+ ]
47
+ try:
48
+ subprocess.run(cmd, check=True)
49
+ print(f"已创建 Windows 计划任务: {task_name}")
50
+ print("下次登录时将自动运行。")
51
+ except subprocess.CalledProcessError as e:
52
+ print(f"创建计划任务失败: {e}")
spothomelight/utils.py ADDED
@@ -0,0 +1,65 @@
1
+ import os
2
+ import sys
3
+ import signal
4
+ import requests
5
+ from io import BytesIO
6
+ from PIL import Image
7
+ import colorgram
8
+ from appdirs import user_cache_dir
9
+
10
+ APP_NAME = "spothomelight"
11
+ PID_FILE = os.path.join(user_cache_dir(APP_NAME), "spothomelight.pid")
12
+
13
+ def get_image_color(image_url):
14
+ try:
15
+ response = requests.get(image_url, timeout=10)
16
+ original_image_data = BytesIO(response.content)
17
+
18
+ with Image.open(original_image_data) as img:
19
+ img = img.convert('RGB')
20
+ img.thumbnail((100, 100))
21
+
22
+ buffer = BytesIO()
23
+ img.save(buffer, format="JPEG")
24
+ buffer.seek(0)
25
+
26
+ colors = colorgram.extract(buffer, 1)
27
+
28
+ if colors:
29
+ rgb = colors[0].rgb
30
+ return (rgb.r, rgb.g, rgb.b)
31
+
32
+ except Exception as e:
33
+ print(f"取色失败: {e}")
34
+ return None
35
+
36
+ def write_pid():
37
+ pid_dir = os.path.dirname(PID_FILE)
38
+ if not os.path.exists(pid_dir):
39
+ os.makedirs(pid_dir)
40
+ with open(PID_FILE, 'w') as f:
41
+ f.write(str(os.getpid()))
42
+
43
+ def check_running():
44
+ if os.path.exists(PID_FILE):
45
+ try:
46
+ with open(PID_FILE, 'r') as f:
47
+ pid = int(f.read().strip())
48
+ os.kill(pid, 0)
49
+ return pid
50
+ except (OSError, ValueError):
51
+ os.remove(PID_FILE)
52
+ return None
53
+
54
+ def stop_process():
55
+ pid = check_running()
56
+ if pid:
57
+ try:
58
+ os.kill(pid, signal.SIGTERM)
59
+ print(f"已停止进程 PID: {pid}")
60
+ if os.path.exists(PID_FILE):
61
+ os.remove(PID_FILE)
62
+ except PermissionError:
63
+ print("权限不足,无法停止进程。")
64
+ else:
65
+ print("服务未运行。")
@@ -0,0 +1,68 @@
1
+ import os
2
+ import sys
3
+ import signal
4
+ import requests
5
+ from io import BytesIO
6
+ from PIL import Image
7
+ from appdirs import user_cache_dir
8
+
9
+ APP_NAME = "spothomelight"
10
+ PID_FILE = os.path.join(user_cache_dir(APP_NAME), "spothomelight.pid")
11
+
12
+ def get_image_color(image_url):
13
+ try:
14
+ response = requests.get(image_url, timeout=10)
15
+ image_data = BytesIO(response.content)
16
+
17
+ with Image.open(image_data) as img:
18
+ img = img.convert('RGB')
19
+
20
+ img.thumbnail((100, 100))
21
+
22
+ p_img = img.quantize(colors=5, method=0)
23
+
24
+ dominant_color = sorted(p_img.getcolors(maxcolors=10), key=lambda x: x[0], reverse=True)[0]
25
+
26
+ palette = p_img.getpalette()
27
+ color_index = dominant_color[1]
28
+
29
+ r = palette[color_index * 3]
30
+ g = palette[color_index * 3 + 1]
31
+ b = palette[color_index * 3 + 2]
32
+
33
+ return (r, g, b)
34
+
35
+ except Exception as e:
36
+ print(f"取色失败: {e}")
37
+ return None
38
+
39
+ def write_pid():
40
+ pid_dir = os.path.dirname(PID_FILE)
41
+ if not os.path.exists(pid_dir):
42
+ os.makedirs(pid_dir)
43
+ with open(PID_FILE, 'w') as f:
44
+ f.write(str(os.getpid()))
45
+
46
+ def check_running():
47
+ if os.path.exists(PID_FILE):
48
+ try:
49
+ with open(PID_FILE, 'r') as f:
50
+ pid = int(f.read().strip())
51
+ os.kill(pid, 0)
52
+ return pid
53
+ except (OSError, ValueError):
54
+ os.remove(PID_FILE)
55
+ return None
56
+
57
+ def stop_process():
58
+ pid = check_running()
59
+ if pid:
60
+ try:
61
+ os.kill(pid, signal.SIGTERM)
62
+ print(f"已停止进程 PID: {pid}")
63
+ if os.path.exists(PID_FILE):
64
+ os.remove(PID_FILE)
65
+ except PermissionError:
66
+ print("权限不足,无法停止进程。")
67
+ else:
68
+ print("服务未运行。")
@@ -0,0 +1,147 @@
1
+ Metadata-Version: 2.4
2
+ Name: spothomelight
3
+ Version: 0.1.0
4
+ Summary: Sync Spotify cover color to Home Assistant via Webhook
5
+ Author-email: "ZGQ Inc." <zgqinc@gmail.com>
6
+ Project-URL: Homepage, https://domain.zgqinc.gq/
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: requests
13
+ Requires-Dist: spotipy
14
+ Requires-Dist: colorgram.py
15
+ Requires-Dist: Pillow
16
+ Requires-Dist: appdirs
17
+
18
+ # SpotHomeLight 🎵🏠💡
19
+
20
+ **SpotHomeLight** 是一个轻量级的 Python 服务,用于将 Spotify 当前播放封面的主色调同步到 Home Assistant。
21
+
22
+ 它运行在你的家用服务器(Linux 或 Windows)上,通过 K-Means 聚类算法提取封面颜色,并通过 Webhook 实时推送到 Home Assistant,让你的智能灯光跟随音乐氛围律动。
23
+
24
+ ## ✨ 功能特性
25
+
26
+ * **跨平台支持**:完美支持 Linux (Systemd) 和 Windows (计划任务)。
27
+ * **无头模式设计**:专为无显示器的家用服务器设计,支持终端内完成 OAuth 认证。
28
+ * **智能取色**:使用 K-Means 聚类算法提取最主要颜色,并进行缩略图预处理以降低 CPU 占用。
29
+ * **开机自启**:内置一键配置开机自动运行 (`-a`)。
30
+ * **低资源占用**:去重机制,仅在切歌时进行下载和计算。
31
+
32
+ ## 🛠️ 前置要求
33
+
34
+ 1. **Home Assistant**: 一个运行中的 HA 实例。
35
+ 2. **Spotify 开发者账号**: 用于获取 API 凭证。
36
+
37
+ ## 📦 安装
38
+
39
+ 使用 pip 安装:
40
+
41
+ ```bash
42
+ pip install spothomelight
43
+ ```
44
+
45
+ ## 🚀 快速开始
46
+
47
+ ### 准备 Spotify API
48
+
49
+ 1. 登录 [Spotify Developer Dashboard](https://developer.spotify.com/dashboard/).
50
+ 2. 创建一个新的 App (例如命名为 `SpotHomeLight`)。
51
+ 3. 将 Redirect URI 设置为:
52
+
53
+ ```
54
+ [http://127.0.0.1:29092/callback](http://127.0.0.1:29092/callback)
55
+ ```
56
+
57
+ 4. 记下 **Client ID** 和 **Client Secret**。
58
+
59
+ ### 初始化配置
60
+
61
+ 运行以下命令打开配置文件:
62
+
63
+ ```bash
64
+ spothomelight -c
65
+ ```
66
+
67
+ conf描述:
68
+
69
+ ```ini
70
+ [SPOTIFY]
71
+ client_id = Client ID
72
+ client_secret = Client Secret
73
+ redirect_uri = 重定向URI(一般不需要修改,除非你知道自己在干什么。)
74
+
75
+ [HOME_ASSISTANT]
76
+ ha_url = Home Assistant 地址(局域网/广域网)
77
+ webhook_id = 从 Home Assistant 获取的 Webhook ID
78
+
79
+ [GENERAL]
80
+ interval = 循环周期(秒)
81
+ ```
82
+
83
+ ### 配置 Home Assistant
84
+
85
+ 登录 Home Assistant 后台,设置,自动化与场景,创建自动化,创建新的自动化,右上角三点菜单,YAML 编辑,将下面的yaml粘贴进去,保存。
86
+
87
+ 右上角三点菜单,可视化编辑,复制 每当 里面的 Webhook ID,注意不要点复制按钮,那会复制API地址,需要选中输入框的内容并复制。
88
+
89
+ 就执行 的实体删除pending,添加目标,选择自己的RGB灯,保存。
90
+
91
+ ```yaml
92
+ alias: Spotify Cover Sync
93
+ description: ""
94
+ mode: restart
95
+ trigger:
96
+ - platform: webhook
97
+ webhook_id: ""
98
+ local_only: true
99
+ condition: []
100
+ action:
101
+ - service: light.turn_on
102
+ target:
103
+ entity_id: light.pending
104
+ data:
105
+ rgb_color: "{{ trigger.json.rgb }}"
106
+ brightness_pct: 100
107
+ transition: 2
108
+ ```
109
+
110
+ ### 首次运行与认证
111
+
112
+ 在终端运行:
113
+
114
+ ```bash
115
+ spothomelight
116
+ ```
117
+
118
+ 考虑到服务器通常没有浏览器,程序会打印一个认证 URL。
119
+
120
+ 1. 复制该 URL 到你电脑的浏览器中打开。
121
+ 2. 登录并点击“同意”。
122
+ 3. 浏览器会跳转到一个 `127.0.0.1` 的无法连接页面。
123
+ 4. 复制浏览器地址栏中完整的 URL。
124
+ 5. 回到服务器终端,粘贴 URL 并回车。
125
+
126
+ 认证成功后,程序应该开始监控并在终端输出日志,观察输出是否正常。
127
+
128
+ ### 第五步:设置开机自启
129
+
130
+ 确认运行正常后,Ctrl+C 停止程序,然后运行:
131
+
132
+ ```bash
133
+ spothomelight -a
134
+ ```
135
+
136
+ * **Linux**: 会自动创建并启用 Systemd User Service。
137
+ * **Windows**: 会自动创建 Windows 计划任务(登录时运行)。
138
+
139
+ 停止服务:
140
+
141
+ ```bash
142
+ spothomelight -s
143
+ ```
144
+
145
+ ## 📝 License
146
+
147
+ MIT License. Copyright (c) 2026 ZGQ Inc.
@@ -0,0 +1,13 @@
1
+ spothomelight/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ spothomelight/auth.py,sha256=TGmR28lIBZnEVkMv7jIDBToZT3rcFUaqv4ZineIO-Do,1706
3
+ spothomelight/config.py,sha256=URC1WwzZN2gRIfwNlMRwHNVtoviayYzmf6F0w3lMhLA,2274
4
+ spothomelight/core.py,sha256=u87Q3BTdwbpjQIT4sZAK9-g79Rr-KHOepSLs2jDufPo,3002
5
+ spothomelight/main.py,sha256=jX8esko2yj8c09HKBb1qmQloWE0mUQEc0F_CL27l5t8,1377
6
+ spothomelight/service.py,sha256=jrlnIn9LL1YIEUrWoiVzPQgEFNvh0L0QBUSqNBMvAOg,1649
7
+ spothomelight/utils.py,sha256=TnGji05MKMTsoqWUq5wFPx6jLthqA-KWVH-dCu5VbIc,1814
8
+ spothomelight/utils_median_cut.py,sha256=9sqUBk0ZpHWwfoPl4YPRcljE_K05w8trZFQo8bVCMds,1991
9
+ spothomelight-0.1.0.dist-info/METADATA,sha256=aSN5SM6XWzXhpX6ivYF3RHVr8blqbc-oXduXWODBxFE,4248
10
+ spothomelight-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
11
+ spothomelight-0.1.0.dist-info/entry_points.txt,sha256=f9haGAoi-aXDTCkoxuSkdN78sHbvfbrr9gwaOIewuvw,58
12
+ spothomelight-0.1.0.dist-info/top_level.txt,sha256=70ecLHIP1BNR5ZLYC-IfQ6ceydH0AZaDGIoH_wGrQZY,14
13
+ spothomelight-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ spothomelight = spothomelight.main:main
@@ -0,0 +1 @@
1
+ spothomelight