spolyrics 1.2.0__tar.gz → 1.2.2__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.
- {spolyrics-1.2.0 → spolyrics-1.2.2}/LICENSE +21 -21
- {spolyrics-1.2.0/spolyrics.egg-info → spolyrics-1.2.2}/PKG-INFO +6 -2
- {spolyrics-1.2.0 → spolyrics-1.2.2}/README.md +130 -126
- {spolyrics-1.2.0 → spolyrics-1.2.2}/main.py +199 -105
- {spolyrics-1.2.0 → spolyrics-1.2.2}/setup.py +1 -1
- {spolyrics-1.2.0 → spolyrics-1.2.2/spolyrics.egg-info}/PKG-INFO +6 -2
- {spolyrics-1.2.0 → spolyrics-1.2.2}/assets.py +0 -0
- {spolyrics-1.2.0 → spolyrics-1.2.2}/setup.cfg +0 -0
- {spolyrics-1.2.0 → spolyrics-1.2.2}/spolyrics.egg-info/SOURCES.txt +0 -0
- {spolyrics-1.2.0 → spolyrics-1.2.2}/spolyrics.egg-info/dependency_links.txt +0 -0
- {spolyrics-1.2.0 → spolyrics-1.2.2}/spolyrics.egg-info/entry_points.txt +0 -0
- {spolyrics-1.2.0 → spolyrics-1.2.2}/spolyrics.egg-info/requires.txt +0 -0
- {spolyrics-1.2.0 → spolyrics-1.2.2}/spolyrics.egg-info/top_level.txt +0 -0
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 paturrr
|
|
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.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 paturrr
|
|
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.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spolyrics
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: A minimalist, borderless, zero-delay synced lyrics miniplayer for Spotify on Windows.
|
|
5
5
|
Author: SpoLyrics Team
|
|
6
6
|
Requires-Python: >=3.8
|
|
@@ -25,7 +25,8 @@ Dynamic: summary
|
|
|
25
25
|
<h1 align="center">SpoLyrics</h1>
|
|
26
26
|
|
|
27
27
|
<p align="center">
|
|
28
|
-
<em>A minimalist, transparent, and zero-delay Spotify lyrics miniplayer.</em>
|
|
28
|
+
<em>A minimalist, transparent, and zero-delay Spotify lyrics miniplayer.</em><br>
|
|
29
|
+
<strong>🌐 <a href="https://paturrr.github.io/SpoLyrics/">Visit the Official Website</a></strong>
|
|
29
30
|
</p>
|
|
30
31
|
|
|
31
32
|
<p align="center">
|
|
@@ -75,6 +76,9 @@ I built this project to solve a specific complaint from my friends: they wanted
|
|
|
75
76
|
|
|
76
77
|
- **Advanced Customization (Settings)**
|
|
77
78
|
Change lyrics color using a modern real-time custom HSV color picker, adjust window transparency (opacity), and enable "Auto-Start with Windows". All configurations are permanently saved.
|
|
79
|
+
|
|
80
|
+
- **Auto Update Checker**
|
|
81
|
+
Automatically checks for the newest version on PyPI when you launch the app, offering a seamless one-click update and restart experience.
|
|
78
82
|
|
|
79
83
|
## Invisible Controls
|
|
80
84
|
|
|
@@ -1,126 +1,130 @@
|
|
|
1
|
-
<p align="center">
|
|
2
|
-
<img src="https://raw.githubusercontent.com/paturrr/SpoLyrics/main/icon.ico" width="150" alt="SpoLyrics Logo">
|
|
3
|
-
</p>
|
|
4
|
-
|
|
5
|
-
<h1 align="center">SpoLyrics</h1>
|
|
6
|
-
|
|
7
|
-
<p align="center">
|
|
8
|
-
<em>A minimalist, transparent, and zero-delay Spotify lyrics miniplayer.</em>
|
|
9
|
-
</
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
<img src="https://img.shields.io/
|
|
14
|
-
<img src="https://img.shields.io/badge/
|
|
15
|
-
<img src="https://img.shields.io/badge
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
<
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
- `
|
|
68
|
-
- `
|
|
69
|
-
- `
|
|
70
|
-
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
```bash
|
|
83
|
-
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
```bash
|
|
105
|
-
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/paturrr/SpoLyrics/main/icon.ico" width="150" alt="SpoLyrics Logo">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">SpoLyrics</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<em>A minimalist, transparent, and zero-delay Spotify lyrics miniplayer.</em><br>
|
|
9
|
+
<strong>🌐 <a href="https://paturrr.github.io/SpoLyrics/">Visit the Official Website</a></strong>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<img src="https://img.shields.io/github/stars/paturrr/SpoLyrics.svg?style=flat-square&color=1DB954&label=stars&logo=github&logoColor=white" alt="Stars">
|
|
14
|
+
<img src="https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20%7C%203.11%20%7C%203.12-1DB954?style=flat-square&logo=python&logoColor=white" alt="Python Versions">
|
|
15
|
+
<img src="https://img.shields.io/badge/OS-Windows%2010%2F11-1DB954?style=flat-square&logo=windows&logoColor=white" alt="Windows Only">
|
|
16
|
+
<img src="https://img.shields.io/badge/%F0%9F%93%9C_License-MIT-1DB954?style=flat-square" alt="MIT license">
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<p align="center">
|
|
20
|
+
<strong>0-Delay Sync · Native Windows API · Glassmorphism UI · Invisible Controls</strong><br>
|
|
21
|
+
<sub>Built with Python & Windows System Media Transport Controls for 100% precision.</sub>
|
|
22
|
+
</p>
|
|
23
|
+
|
|
24
|
+
<p align="center">
|
|
25
|
+
<img src="https://raw.githubusercontent.com/paturrr/SpoLyrics/main/assets/1.png" alt="SpoLyrics Transparent UI & Lyrics Sync">
|
|
26
|
+
<br><br>
|
|
27
|
+
<img src="https://raw.githubusercontent.com/paturrr/SpoLyrics/main/assets/2.png" alt="SpoLyrics Modern Settings & Shortcuts">
|
|
28
|
+
</p>
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Background & Feedback
|
|
33
|
+
|
|
34
|
+
I built this project to solve a specific complaint from my friends: they wanted to listen to songs on Spotify or YouTube Music while working, but the official miniplayers didn't show any lyrics.
|
|
35
|
+
|
|
36
|
+
**Note:** This application is still in active development! If you encounter any bugs, have a feature request, or just want to suggest an improvement, please don't hesitate to open an issue or let me know.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Key Features
|
|
41
|
+
|
|
42
|
+
- **Zero-Delay Sync**
|
|
43
|
+
100% precise time synchronization. Pulls real-time duration data directly from the Windows OS core.
|
|
44
|
+
|
|
45
|
+
- **Smart Duration Match**
|
|
46
|
+
Automatically matches lyrics based on the duration of the currently playing song version (solving the Radio Edit vs. Album Version mismatch).
|
|
47
|
+
|
|
48
|
+
- **Glassmorphism UI**
|
|
49
|
+
Elegant, transparent, and borderless design.
|
|
50
|
+
|
|
51
|
+
- **Freely Draggable**
|
|
52
|
+
Left-click and hold anywhere to drag the lyrics window wherever you like.
|
|
53
|
+
|
|
54
|
+
- **System Tray Integration**
|
|
55
|
+
Runs quietly in the background. Right-click the system tray icon (SpoLyrics logo) to access settings, hide the lyrics, or view shortcuts.
|
|
56
|
+
|
|
57
|
+
- **Advanced Customization (Settings)**
|
|
58
|
+
Change lyrics color using a modern real-time custom HSV color picker, adjust window transparency (opacity), and enable "Auto-Start with Windows". All configurations are permanently saved.
|
|
59
|
+
|
|
60
|
+
- **Auto Update Checker**
|
|
61
|
+
Automatically checks for the newest version on PyPI when you launch the app, offering a seamless one-click update and restart experience.
|
|
62
|
+
|
|
63
|
+
## Invisible Controls
|
|
64
|
+
|
|
65
|
+
This application uses zero conventional buttons to keep the UI perfectly clean. Use these mouse gestures:
|
|
66
|
+
|
|
67
|
+
- `Right Click`: Play / Pause
|
|
68
|
+
- `Mouse Scroll (Up/Down)`: Next / Previous track
|
|
69
|
+
- `Middle Click (Scroll Wheel or 3-Finger Tap on Touchpad)`: Toggle Pin / Lock Window
|
|
70
|
+
- *When locked, the resize icon changes to a padlock (🔒) and the app ignores all drags/clicks to prevent accidental touches. Middle click again to unlock.*
|
|
71
|
+
- `Ctrl + Left Click`: Toggle song title visibility
|
|
72
|
+
- `Ctrl + Mouse Scroll`: Increase / Decrease font size
|
|
73
|
+
- `Drag Bottom Right Corner (⇲)`: Resize the window
|
|
74
|
+
- `Double Left Click`: Close the application
|
|
75
|
+
|
|
76
|
+
## Installation
|
|
77
|
+
|
|
78
|
+
Run this single command in your Windows Terminal or Command Prompt (Requires **Python 3.8 to 3.12**):
|
|
79
|
+
|
|
80
|
+
> **Note on Python Version:** Do NOT use Python 3.13. The required `winsdk` package does not yet have pre-compiled binaries for Python 3.13 and will fail to install. Please stick to Python 3.12 or older.
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pip install spolyrics
|
|
84
|
+
```
|
|
85
|
+
*(If you get a `pip is not recognized` error, use this alternative command instead:)*
|
|
86
|
+
```bash
|
|
87
|
+
python -m pip install spolyrics
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
> **IMPORTANT (OS Support):**
|
|
91
|
+
> This app is **Exclusive to Windows**. Because it uses `winsdk` (Windows System Media Transport Controls) to achieve 0-delay performance without third-party servers, it **CANNOT** be installed on macOS or Linux.
|
|
92
|
+
|
|
93
|
+
### Alternative Installation (Manual)
|
|
94
|
+
|
|
95
|
+
If you prefer to download the source code manually (e.g., as a `.zip` file) instead of using the direct `git` command above, you must install the required dependencies (the "shopping list") yourself.
|
|
96
|
+
Open your terminal inside the `SpoLyrics` folder and run:
|
|
97
|
+
```bash
|
|
98
|
+
pip install -r requirements.txt
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Usage
|
|
102
|
+
|
|
103
|
+
Once installed, you can launch the app from any directory by typing:
|
|
104
|
+
```bash
|
|
105
|
+
spolyrics
|
|
106
|
+
```
|
|
107
|
+
Or, if you want it to run smoothly without spawning a black background terminal:
|
|
108
|
+
```bash
|
|
109
|
+
pythonw -m main
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
*(Make sure Spotify or your Windows media player is running and playing a song!)*
|
|
113
|
+
|
|
114
|
+
## Updating
|
|
115
|
+
|
|
116
|
+
To update the application to the latest version, run:
|
|
117
|
+
```bash
|
|
118
|
+
pip install --upgrade spolyrics
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Uninstallation
|
|
122
|
+
|
|
123
|
+
If you wish to remove this application from your computer, simply run:
|
|
124
|
+
```bash
|
|
125
|
+
pip uninstall spolyrics
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
This project is licensed under the [MIT License](LICENSE) - see the LICENSE file for details.
|
|
@@ -12,8 +12,10 @@ import logging
|
|
|
12
12
|
import sqlite3
|
|
13
13
|
from datetime import datetime, timezone
|
|
14
14
|
from assets import ICON_B64
|
|
15
|
+
from tkinter import messagebox
|
|
15
16
|
from winsdk.windows.media.control import GlobalSystemMediaTransportControlsSessionManager as MediaManager
|
|
16
17
|
|
|
18
|
+
CURRENT_VERSION = "1.2.2"
|
|
17
19
|
CONFIG_PATH = os.path.join(os.environ.get("APPDATA", ""), "SpoLyrics", "config.json")
|
|
18
20
|
APP_DIR = os.path.join(os.environ.get("APPDATA", ""), "SpoLyrics")
|
|
19
21
|
os.makedirs(APP_DIR, exist_ok=True)
|
|
@@ -85,19 +87,25 @@ def save_config(config):
|
|
|
85
87
|
with open(CONFIG_PATH, 'w') as f:
|
|
86
88
|
json.dump(config, f)
|
|
87
89
|
|
|
88
|
-
def
|
|
90
|
+
def get_exe_path():
|
|
91
|
+
import sys
|
|
92
|
+
if getattr(sys, 'frozen', False):
|
|
93
|
+
return sys.executable
|
|
94
|
+
script_exe = os.path.join(sys.prefix, 'Scripts', 'spolyrics.exe')
|
|
95
|
+
if os.path.exists(script_exe):
|
|
96
|
+
return script_exe
|
|
97
|
+
return sys.executable
|
|
98
|
+
|
|
99
|
+
def set_auto_start(enable, force_update=False):
|
|
89
100
|
try:
|
|
90
101
|
if enable:
|
|
91
102
|
app_dir = os.path.join(os.environ.get("APPDATA", ""), "SpoLyrics")
|
|
92
103
|
icon_path = os.path.join(app_dir, 'icon.ico')
|
|
93
|
-
|
|
94
|
-
if getattr(sys, 'frozen', False):
|
|
95
|
-
exe_path = sys.executable
|
|
96
|
-
else:
|
|
97
|
-
exe_path = shutil.which('spolyrics') or sys.executable
|
|
104
|
+
exe_path = get_exe_path()
|
|
98
105
|
if exe_path and os.path.exists(icon_path):
|
|
99
|
-
|
|
100
|
-
|
|
106
|
+
if force_update or not os.path.exists(STARTUP_SHORTCUT):
|
|
107
|
+
ps_script = f"$s=(New-Object -COM WScript.Shell).CreateShortcut('{STARTUP_SHORTCUT}');$s.TargetPath='{exe_path}';$s.IconLocation='{icon_path}';$s.Save()"
|
|
108
|
+
subprocess.run(["powershell", "-Command", ps_script], creationflags=0x08000000)
|
|
101
109
|
else:
|
|
102
110
|
if os.path.exists(STARTUP_SHORTCUT):
|
|
103
111
|
os.remove(STARTUP_SHORTCUT)
|
|
@@ -130,7 +138,7 @@ class MiniLyrics:
|
|
|
130
138
|
self.is_pinned = False
|
|
131
139
|
self.show_title = True
|
|
132
140
|
|
|
133
|
-
self.lbl_title = tk.Label(self.root, text="", fg='#a0a0a0', bg='#191414', font=('
|
|
141
|
+
self.lbl_title = tk.Label(self.root, text="", fg='#a0a0a0', bg='#191414', font=('Segoe UI', 8, 'bold'), justify='center', wraplength=320)
|
|
134
142
|
self.lbl_title.place(relx=0.5, y=8, anchor='n')
|
|
135
143
|
|
|
136
144
|
self.lbl_current = tk.Label(self.root, text="Waiting for song...", fg=self.config['color'], bg='#191414', font=('Arial', self.font_cur, 'bold'), wraplength=330, justify="center")
|
|
@@ -138,11 +146,16 @@ class MiniLyrics:
|
|
|
138
146
|
|
|
139
147
|
self.lbl_next = tk.Label(self.root, text="", fg='#b3b3b3', bg='#191414', font=('Arial', self.font_nxt), wraplength=330, justify="center")
|
|
140
148
|
self.lbl_next.pack(expand=True, fill='both', padx=10, pady=(0, 15))
|
|
149
|
+
|
|
150
|
+
self.update_available = False
|
|
151
|
+
self.root.after(2000, self.check_updates)
|
|
141
152
|
|
|
142
153
|
self.grip = tk.Label(self.root, text="⇲", fg='#333333', bg='#191414', cursor="size_nw_se")
|
|
143
154
|
self.grip.place(relx=1.0, rely=1.0, anchor="se")
|
|
144
155
|
|
|
145
156
|
self.grip.bind("<B1-Motion>", self.on_resize)
|
|
157
|
+
self.grip.bind("<Enter>", lambda e: not self.is_pinned and self.grip.config(fg='#888888'))
|
|
158
|
+
self.grip.bind("<Leave>", lambda e: not self.is_pinned and self.grip.config(fg='#333333'))
|
|
146
159
|
self.root.bind("<B1-Motion>", self.drag)
|
|
147
160
|
self.root.bind("<Button-1>", self.click)
|
|
148
161
|
self.root.bind("<Double-1>", lambda e: not self.is_pinned and self.root.destroy())
|
|
@@ -248,6 +261,16 @@ class MiniLyrics:
|
|
|
248
261
|
|
|
249
262
|
def on_scroll(self, event):
|
|
250
263
|
if self.is_pinned: return "break"
|
|
264
|
+
|
|
265
|
+
now = time.time()
|
|
266
|
+
if not hasattr(self, 'last_scroll_time'):
|
|
267
|
+
self.last_scroll_time = 0
|
|
268
|
+
|
|
269
|
+
if now - self.last_scroll_time < 0.8:
|
|
270
|
+
return "break"
|
|
271
|
+
|
|
272
|
+
self.last_scroll_time = now
|
|
273
|
+
|
|
251
274
|
if event.delta > 0: self.media_control('prev')
|
|
252
275
|
else: self.media_control('next')
|
|
253
276
|
return "break"
|
|
@@ -270,7 +293,7 @@ class MiniLyrics:
|
|
|
270
293
|
|
|
271
294
|
self.info_win = tk.Toplevel(self.root)
|
|
272
295
|
self.info_win.title("SpoLyrics Info")
|
|
273
|
-
self.info_win.geometry("
|
|
296
|
+
self.info_win.geometry("420x400")
|
|
274
297
|
self.info_win.configure(bg='#191414')
|
|
275
298
|
self.info_win.attributes('-topmost', True)
|
|
276
299
|
self.info_win.overrideredirect(True)
|
|
@@ -337,7 +360,7 @@ class MiniLyrics:
|
|
|
337
360
|
|
|
338
361
|
self.settings_win = tk.Toplevel(self.root)
|
|
339
362
|
self.settings_win.title("SpoLyrics Settings")
|
|
340
|
-
self.settings_win.geometry("
|
|
363
|
+
self.settings_win.geometry("380x520")
|
|
341
364
|
self.settings_win.configure(bg='#191414')
|
|
342
365
|
self.settings_win.attributes('-topmost', True)
|
|
343
366
|
self.settings_win.resizable(False, False)
|
|
@@ -350,27 +373,29 @@ class MiniLyrics:
|
|
|
350
373
|
self.settings_win.destroy()
|
|
351
374
|
self.settings_win.protocol("WM_DELETE_WINDOW", on_settings_close)
|
|
352
375
|
|
|
353
|
-
tk.
|
|
376
|
+
header_frame = tk.Frame(self.settings_win, bg='#191414')
|
|
377
|
+
header_frame.pack(fill='x', padx=25, pady=(20, 15))
|
|
378
|
+
tk.Label(header_frame, text="Settings", fg='white', bg='#191414', font=('Segoe UI', 18, 'bold')).pack(side='left')
|
|
354
379
|
|
|
355
|
-
info_lbl = tk.Label(
|
|
356
|
-
info_lbl.
|
|
380
|
+
info_lbl = tk.Label(header_frame, text="❔", fg='#888888', bg='#191414', font=('Segoe UI', 12), cursor='hand2')
|
|
381
|
+
info_lbl.pack(side='right', pady=(5,0))
|
|
357
382
|
info_lbl.bind("<Button-1>", lambda e: self.open_info())
|
|
358
383
|
info_lbl.bind("<Enter>", lambda e: info_lbl.config(fg='white'))
|
|
359
384
|
info_lbl.bind("<Leave>", lambda e: info_lbl.config(fg='#888888'))
|
|
360
385
|
|
|
361
|
-
#
|
|
362
|
-
|
|
363
|
-
|
|
386
|
+
tk.Label(self.settings_win, text="APPEARANCE", fg='#888888', bg='#191414', font=('Segoe UI', 8, 'bold')).pack(anchor='w', padx=25, pady=(0, 5))
|
|
387
|
+
app_card = tk.Frame(self.settings_win, bg='#242424')
|
|
388
|
+
app_card.pack(fill='x', padx=25, pady=(0, 15))
|
|
364
389
|
|
|
365
|
-
|
|
390
|
+
color_frame = tk.Frame(app_card, bg='#242424')
|
|
391
|
+
color_frame.pack(fill='x', padx=15, pady=12)
|
|
392
|
+
tk.Label(color_frame, text="Lyrics Color", fg='#e0e0e0', bg='#242424', font=('Segoe UI', 10)).pack(side='left')
|
|
366
393
|
|
|
367
394
|
self.current_hex = self.config['color']
|
|
368
|
-
|
|
369
|
-
color_btn = tk.Frame(color_frame, bg=self.current_hex, width=50, height=25, cursor='hand2', highlightbackground='#333333', highlightthickness=1)
|
|
395
|
+
color_btn = tk.Frame(color_frame, bg=self.current_hex, width=24, height=24, cursor='hand2', highlightbackground='#555', highlightthickness=1)
|
|
370
396
|
color_btn.pack(side='right')
|
|
371
397
|
|
|
372
|
-
|
|
373
|
-
palette_frame = tk.Frame(self.settings_win, bg='#191414')
|
|
398
|
+
palette_frame = tk.Frame(app_card, bg='#1e1e1e')
|
|
374
399
|
self.palette_visible = False
|
|
375
400
|
|
|
376
401
|
import colorsys
|
|
@@ -378,8 +403,8 @@ class MiniLyrics:
|
|
|
378
403
|
rgb = colorsys.hls_to_rgb(h, l, s)
|
|
379
404
|
return "#{:02x}{:02x}{:02x}".format(int(rgb[0]*255), int(rgb[1]*255), int(rgb[2]*255))
|
|
380
405
|
|
|
381
|
-
tk.Label(palette_frame, text="Hue
|
|
382
|
-
hue_canvas = tk.Canvas(palette_frame, width=240, height=12, bg='#
|
|
406
|
+
tk.Label(palette_frame, text="Hue:", fg='#888', bg='#1e1e1e', font=('Segoe UI', 8)).pack(anchor='w', padx=15, pady=(10,2))
|
|
407
|
+
hue_canvas = tk.Canvas(palette_frame, width=240, height=12, bg='#1e1e1e', highlightthickness=0, cursor='hand2')
|
|
383
408
|
hue_canvas.pack(pady=(0, 5))
|
|
384
409
|
|
|
385
410
|
for i in range(120):
|
|
@@ -387,9 +412,9 @@ class MiniLyrics:
|
|
|
387
412
|
hue_canvas.create_rectangle(i*2, 0, (i+1)*2, 12, fill=c, outline=c)
|
|
388
413
|
hue_thumb = hue_canvas.create_rectangle(0, 0, 4, 12, fill='white', outline='black')
|
|
389
414
|
|
|
390
|
-
tk.Label(palette_frame, text="Lightness
|
|
391
|
-
lit_canvas = tk.Canvas(palette_frame, width=240, height=12, bg='#
|
|
392
|
-
lit_canvas.pack(pady=(0,
|
|
415
|
+
tk.Label(palette_frame, text="Lightness:", fg='#888', bg='#1e1e1e', font=('Segoe UI', 8)).pack(anchor='w', padx=15, pady=(5,2))
|
|
416
|
+
lit_canvas = tk.Canvas(palette_frame, width=240, height=12, bg='#1e1e1e', highlightthickness=0, cursor='hand2')
|
|
417
|
+
lit_canvas.pack(pady=(0, 10))
|
|
393
418
|
|
|
394
419
|
lit_rects = []
|
|
395
420
|
for i in range(120):
|
|
@@ -397,10 +422,10 @@ class MiniLyrics:
|
|
|
397
422
|
lit_rects.append(r)
|
|
398
423
|
lit_thumb = lit_canvas.create_rectangle(120, 0, 124, 12, fill='black', outline='white')
|
|
399
424
|
|
|
400
|
-
custom_frame = tk.Frame(palette_frame, bg='#
|
|
401
|
-
custom_frame.pack(pady=(
|
|
402
|
-
tk.Label(custom_frame, text="Hex:", fg='#888', bg='#
|
|
403
|
-
hex_entry = tk.Entry(custom_frame, width=9, bg='#
|
|
425
|
+
custom_frame = tk.Frame(palette_frame, bg='#1e1e1e')
|
|
426
|
+
custom_frame.pack(pady=(0, 15))
|
|
427
|
+
tk.Label(custom_frame, text="Hex:", fg='#888', bg='#1e1e1e', font=('Segoe UI', 9)).pack(side='left')
|
|
428
|
+
hex_entry = tk.Entry(custom_frame, width=9, bg='#333', fg='white', insertbackground='white', relief='flat', justify='center')
|
|
404
429
|
hex_entry.insert(0, self.current_hex)
|
|
405
430
|
hex_entry.pack(side='left', padx=5)
|
|
406
431
|
|
|
@@ -446,41 +471,39 @@ class MiniLyrics:
|
|
|
446
471
|
|
|
447
472
|
def toggle_palette(e=None):
|
|
448
473
|
if not self.palette_visible:
|
|
449
|
-
self.settings_win.geometry("
|
|
474
|
+
self.settings_win.geometry("380x680")
|
|
450
475
|
update_lit_canvas()
|
|
451
|
-
palette_frame.pack(fill='x',
|
|
476
|
+
palette_frame.pack(fill='x', after=color_frame)
|
|
452
477
|
hex_entry.delete(0, 'end')
|
|
453
478
|
hex_entry.insert(0, self.current_hex)
|
|
454
479
|
else:
|
|
455
|
-
self.settings_win.geometry("
|
|
480
|
+
self.settings_win.geometry("380x520")
|
|
456
481
|
palette_frame.pack_forget()
|
|
457
482
|
self.palette_visible = not self.palette_visible
|
|
458
483
|
|
|
459
484
|
color_btn.bind("<Button-1>", toggle_palette)
|
|
460
485
|
|
|
461
|
-
|
|
462
|
-
op_frame = tk.Frame(self.settings_win, bg='#191414')
|
|
463
|
-
op_frame.pack(fill='x', padx=30, pady=(15, 0))
|
|
486
|
+
tk.Frame(app_card, height=1, bg='#333').pack(fill='x', padx=15)
|
|
464
487
|
|
|
465
|
-
|
|
466
|
-
|
|
488
|
+
op_frame = tk.Frame(app_card, bg='#242424')
|
|
489
|
+
op_frame.pack(fill='x', padx=15, pady=12)
|
|
490
|
+
tk.Label(op_frame, text="Window Opacity", fg='#e0e0e0', bg='#242424', font=('Segoe UI', 10)).pack(side='left')
|
|
467
491
|
|
|
468
492
|
self.cur_op = self.config.get('opacity', 0.85)
|
|
469
|
-
op_val_lbl = tk.Label(op_frame, text=f"{int(self.cur_op*100)}%", fg='
|
|
493
|
+
op_val_lbl = tk.Label(op_frame, text=f"{int(self.cur_op*100)}%", fg='#888', bg='#242424', font=('Segoe UI', 9, 'bold'))
|
|
470
494
|
op_val_lbl.pack(side='right')
|
|
471
495
|
|
|
472
|
-
op_canvas = tk.Canvas(
|
|
473
|
-
op_canvas.pack(pady=(
|
|
474
|
-
|
|
475
|
-
op_canvas.create_rectangle(
|
|
476
|
-
|
|
477
|
-
th_x = int(self.cur_op*320)
|
|
496
|
+
op_canvas = tk.Canvas(app_card, width=330, height=14, bg='#242424', highlightthickness=0, cursor='hand2')
|
|
497
|
+
op_canvas.pack(pady=(0, 15))
|
|
498
|
+
op_canvas.create_rectangle(20, 5, 310, 9, fill='#333', outline='#333', width=0)
|
|
499
|
+
op_fill = op_canvas.create_rectangle(20, 5, int(self.cur_op*290)+20, 9, fill='#1DB954', outline='#1DB954', width=0)
|
|
500
|
+
th_x = int(self.cur_op*290)+20
|
|
478
501
|
op_thumb = op_canvas.create_oval(th_x-6, 1, th_x+6, 13, fill='white', outline='white')
|
|
479
502
|
|
|
480
503
|
def on_op_drag(e):
|
|
481
|
-
x = max(
|
|
482
|
-
self.cur_op = x /
|
|
483
|
-
op_canvas.coords(op_fill,
|
|
504
|
+
x = max(20+29, min(e.x, 310))
|
|
505
|
+
self.cur_op = (x-20) / 290.0
|
|
506
|
+
op_canvas.coords(op_fill, 20, 5, x, 9)
|
|
484
507
|
op_canvas.coords(op_thumb, x-6, 1, x+6, 13)
|
|
485
508
|
op_val_lbl.config(text=f"{int(self.cur_op*100)}%")
|
|
486
509
|
self.root.attributes('-alpha', self.cur_op)
|
|
@@ -488,20 +511,22 @@ class MiniLyrics:
|
|
|
488
511
|
op_canvas.bind("<B1-Motion>", on_op_drag)
|
|
489
512
|
op_canvas.bind("<Button-1>", on_op_drag)
|
|
490
513
|
|
|
491
|
-
#
|
|
492
|
-
|
|
493
|
-
|
|
514
|
+
tk.Label(self.settings_win, text="SYSTEM", fg='#888888', bg='#191414', font=('Segoe UI', 8, 'bold')).pack(anchor='w', padx=25, pady=(5, 5))
|
|
515
|
+
sys_card = tk.Frame(self.settings_win, bg='#242424')
|
|
516
|
+
sys_card.pack(fill='x', padx=25, pady=(0, 15))
|
|
494
517
|
|
|
495
|
-
|
|
518
|
+
toggle_frame = tk.Frame(sys_card, bg='#242424')
|
|
519
|
+
toggle_frame.pack(fill='x', padx=15, pady=12)
|
|
520
|
+
tk.Label(toggle_frame, text="Auto-start Windows", fg='#e0e0e0', bg='#242424', font=('Segoe UI', 10)).pack(side='left')
|
|
496
521
|
|
|
497
522
|
class ToggleSwitch(tk.Canvas):
|
|
498
523
|
def __init__(self, parent, initial_state=False, *args, **kwargs):
|
|
499
|
-
tk.Canvas.__init__(self, parent, width=
|
|
524
|
+
tk.Canvas.__init__(self, parent, width=40, height=20, bg='#242424', highlightthickness=0, cursor='hand2', *args, **kwargs)
|
|
500
525
|
self.is_on = initial_state
|
|
501
|
-
self.track = self.create_oval(2, 2,
|
|
502
|
-
self.track2 = self.create_oval(
|
|
503
|
-
self.rect = self.create_rectangle(
|
|
504
|
-
self.thumb = self.create_oval(4, 4,
|
|
526
|
+
self.track = self.create_oval(2, 2, 18, 18, fill="#555", outline="#555")
|
|
527
|
+
self.track2 = self.create_oval(22, 2, 38, 18, fill="#555", outline="#555")
|
|
528
|
+
self.rect = self.create_rectangle(10, 2, 30, 18, fill="#555", outline="#555")
|
|
529
|
+
self.thumb = self.create_oval(4, 4, 16, 16, fill="white", outline="white")
|
|
505
530
|
self.bind("<Button-1>", self.toggle)
|
|
506
531
|
self._update_ui()
|
|
507
532
|
|
|
@@ -514,35 +539,35 @@ class MiniLyrics:
|
|
|
514
539
|
self.itemconfig(self.track, fill="#1DB954", outline="#1DB954")
|
|
515
540
|
self.itemconfig(self.track2, fill="#1DB954", outline="#1DB954")
|
|
516
541
|
self.itemconfig(self.rect, fill="#1DB954", outline="#1DB954")
|
|
517
|
-
self.coords(self.thumb,
|
|
542
|
+
self.coords(self.thumb, 24, 4, 36, 16)
|
|
518
543
|
else:
|
|
519
|
-
self.itemconfig(self.track, fill="#
|
|
520
|
-
self.itemconfig(self.track2, fill="#
|
|
521
|
-
self.itemconfig(self.rect, fill="#
|
|
522
|
-
self.coords(self.thumb, 4, 4,
|
|
544
|
+
self.itemconfig(self.track, fill="#555", outline="#555")
|
|
545
|
+
self.itemconfig(self.track2, fill="#555", outline="#555")
|
|
546
|
+
self.itemconfig(self.rect, fill="#555", outline="#555")
|
|
547
|
+
self.coords(self.thumb, 4, 4, 16, 16)
|
|
523
548
|
|
|
524
549
|
toggle = ToggleSwitch(toggle_frame, initial_state=self.config['auto_start'])
|
|
525
550
|
toggle.pack(side='right')
|
|
526
|
-
|
|
527
|
-
# --- Cache Info ---
|
|
528
|
-
cache_frame = tk.Frame(self.settings_win, bg='#191414')
|
|
529
|
-
cache_frame.pack(fill='x', padx=30, pady=5)
|
|
530
551
|
|
|
531
|
-
tk.
|
|
552
|
+
tk.Frame(sys_card, height=1, bg='#333').pack(fill='x', padx=15)
|
|
553
|
+
|
|
554
|
+
cache_frame = tk.Frame(sys_card, bg='#242424')
|
|
555
|
+
cache_frame.pack(fill='x', padx=15, pady=12)
|
|
556
|
+
tk.Label(cache_frame, text="Lyrics Cache", fg='#e0e0e0', bg='#242424', font=('Segoe UI', 10)).pack(side='left')
|
|
532
557
|
|
|
533
558
|
def get_db_size():
|
|
534
559
|
try:
|
|
535
560
|
if os.path.exists(CACHE_DB_PATH):
|
|
536
561
|
size_bytes = os.path.getsize(CACHE_DB_PATH)
|
|
537
|
-
if size_bytes >= 1024 * 1024:
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
return f"{size_bytes / 1024:.2f} KB"
|
|
541
|
-
except Exception:
|
|
542
|
-
pass
|
|
562
|
+
if size_bytes >= 1024 * 1024: return f"{size_bytes / (1024 * 1024):.2f} MB"
|
|
563
|
+
else: return f"{size_bytes / 1024:.2f} KB"
|
|
564
|
+
except Exception: pass
|
|
543
565
|
return "0.00 KB"
|
|
544
566
|
|
|
545
|
-
|
|
567
|
+
del_btn = tk.Label(cache_frame, text="Clear", bg='#333', fg='#ff4444', font=('Segoe UI', 8, 'bold'), padx=8, pady=2, cursor='hand2')
|
|
568
|
+
del_btn.pack(side='right')
|
|
569
|
+
|
|
570
|
+
cache_size_lbl = tk.Label(cache_frame, text=get_db_size(), fg='#888', bg='#242424', font=('Segoe UI', 9))
|
|
546
571
|
cache_size_lbl.pack(side='right', padx=(0, 10))
|
|
547
572
|
|
|
548
573
|
def delete_cache(e=None):
|
|
@@ -558,14 +583,10 @@ class MiniLyrics:
|
|
|
558
583
|
except Exception as ex:
|
|
559
584
|
logging.error("Failed to delete cache", exc_info=ex)
|
|
560
585
|
|
|
561
|
-
del_btn = tk.Label(cache_frame, text="Clear", bg='#e91429', fg='white', font=('Segoe UI', 9, 'bold'), padx=10, pady=2, cursor='hand2')
|
|
562
|
-
del_btn.pack(side='right')
|
|
563
586
|
del_btn.bind("<Button-1>", delete_cache)
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
tk.Frame(self.settings_win, height=1, bg='#282828').pack(fill='x', padx=30, pady=10)
|
|
587
|
+
del_btn.bind("<Enter>", lambda e: del_btn.config(bg='#ff4444', fg='white'))
|
|
588
|
+
del_btn.bind("<Leave>", lambda e: del_btn.config(bg='#333', fg='#ff4444'))
|
|
567
589
|
|
|
568
|
-
# --- Save Button ---
|
|
569
590
|
def save_and_apply(e=None):
|
|
570
591
|
self.config['color'] = self.current_hex
|
|
571
592
|
self.config['auto_start'] = toggle.is_on
|
|
@@ -580,15 +601,12 @@ class MiniLyrics:
|
|
|
580
601
|
self.settings_win.protocol("WM_DELETE_WINDOW", lambda: None)
|
|
581
602
|
self.root.after(100, lambda: self.settings_win.destroy() if self.settings_win.winfo_exists() else None)
|
|
582
603
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
save_btn = tk.Label(self.settings_win, text="Save & Apply", bg='#1DB954', fg='white', font=('Segoe UI', 11, 'bold'), pady=8, cursor='hand2')
|
|
587
|
-
save_btn.pack(fill='x', padx=40, pady=(0, 20))
|
|
604
|
+
save_btn = tk.Label(self.settings_win, text="Save & Apply", bg='#1DB954', fg='white', font=('Segoe UI', 10, 'bold'), pady=8, cursor='hand2')
|
|
605
|
+
save_btn.pack(fill='x', padx=25, pady=(5, 20))
|
|
588
606
|
|
|
589
607
|
save_btn.bind("<Button-1>", save_and_apply)
|
|
590
|
-
save_btn.bind("<Enter>",
|
|
591
|
-
save_btn.bind("<Leave>",
|
|
608
|
+
save_btn.bind("<Enter>", lambda e: save_btn.config(bg='#1ed760'))
|
|
609
|
+
save_btn.bind("<Leave>", lambda e: save_btn.config(bg='#1DB954'))
|
|
592
610
|
|
|
593
611
|
def media_control(self, action):
|
|
594
612
|
if self.is_pinned: return "break"
|
|
@@ -706,7 +724,7 @@ class MiniLyrics:
|
|
|
706
724
|
if song_id != self.current_song:
|
|
707
725
|
self.current_song = song_id
|
|
708
726
|
self.root.after(0, lambda sid=song_id: self.lbl_title.config(text=sid if self.show_title else ""))
|
|
709
|
-
self.root.after(0, self.update_ui,
|
|
727
|
+
self.root.after(0, self.update_ui, "Loading lyrics... ⏳", "")
|
|
710
728
|
|
|
711
729
|
async def fetch_and_apply(sid, t, a, d):
|
|
712
730
|
await asyncio.sleep(0.5)
|
|
@@ -724,7 +742,7 @@ class MiniLyrics:
|
|
|
724
742
|
if self.current_song:
|
|
725
743
|
self.current_song = ""
|
|
726
744
|
self.synced_lyrics = []
|
|
727
|
-
self.root.after(0, self.update_ui, "
|
|
745
|
+
self.root.after(0, self.update_ui, "No song playing.", "")
|
|
728
746
|
|
|
729
747
|
except Exception as e:
|
|
730
748
|
logging.error("Error in poll_song loop", exc_info=e)
|
|
@@ -733,7 +751,47 @@ class MiniLyrics:
|
|
|
733
751
|
|
|
734
752
|
asyncio.run(main_loop())
|
|
735
753
|
|
|
754
|
+
def check_updates(self):
|
|
755
|
+
def _check():
|
|
756
|
+
try:
|
|
757
|
+
resp = requests.get('https://pypi.org/pypi/spolyrics/json', timeout=5).json()
|
|
758
|
+
latest = resp['info']['version']
|
|
759
|
+
if latest != CURRENT_VERSION:
|
|
760
|
+
self.root.after(0, self.show_update_prompt, latest)
|
|
761
|
+
except Exception as e:
|
|
762
|
+
logging.error("Failed to check for updates", exc_info=e)
|
|
763
|
+
threading.Thread(target=_check, daemon=True).start()
|
|
764
|
+
|
|
765
|
+
def prompt_update(self):
|
|
766
|
+
if self.update_available:
|
|
767
|
+
self.show_update_prompt(self.update_available)
|
|
768
|
+
|
|
769
|
+
def show_update_prompt(self, latest):
|
|
770
|
+
self.update_available = latest
|
|
771
|
+
if getattr(self, 'tray_icon', None):
|
|
772
|
+
self.tray_icon.update_menu()
|
|
773
|
+
|
|
774
|
+
res = messagebox.askyesno("Update Available", f"A new version of SpoLyrics (v{latest}) is available!\n\nYou are currently on v{CURRENT_VERSION}.\nWould you like to update now?")
|
|
775
|
+
if res:
|
|
776
|
+
import tempfile
|
|
777
|
+
bat_path = os.path.join(tempfile.gettempdir(), "update_spolyrics.bat")
|
|
778
|
+
exe_path = get_exe_path()
|
|
779
|
+
with open(bat_path, 'w') as f:
|
|
780
|
+
f.write('@echo off\n')
|
|
781
|
+
f.write('echo Updating SpoLyrics...\n')
|
|
782
|
+
f.write('ping 127.0.0.1 -n 3 > nul\n')
|
|
783
|
+
f.write('python -m pip install --upgrade spolyrics\n')
|
|
784
|
+
f.write(f'start "" "{exe_path}"\n')
|
|
785
|
+
f.write('del "%~f0"\n')
|
|
786
|
+
subprocess.Popen(['cmd.exe', '/c', bat_path], creationflags=16)
|
|
787
|
+
if getattr(self, 'tray_icon', None):
|
|
788
|
+
self.tray_icon.stop()
|
|
789
|
+
self.root.quit()
|
|
790
|
+
import sys
|
|
791
|
+
sys.exit(0)
|
|
792
|
+
|
|
736
793
|
def create_shortcut():
|
|
794
|
+
path_changed = False
|
|
737
795
|
try:
|
|
738
796
|
shortcut_path = os.path.join(os.environ.get("APPDATA", ""), "Microsoft", "Windows", "Start Menu", "Programs", "SpoLyrics.lnk")
|
|
739
797
|
app_dir = os.path.join(os.environ.get("APPDATA", ""), "SpoLyrics")
|
|
@@ -750,17 +808,26 @@ def create_shortcut():
|
|
|
750
808
|
if not os.path.exists(shortcut_path):
|
|
751
809
|
needs_update = True
|
|
752
810
|
|
|
811
|
+
exe_path = get_exe_path()
|
|
812
|
+
path_file = os.path.join(app_dir, 'current_exe_path.txt')
|
|
813
|
+
saved_path = ""
|
|
814
|
+
if os.path.exists(path_file):
|
|
815
|
+
with open(path_file, 'r') as f:
|
|
816
|
+
saved_path = f.read().strip()
|
|
817
|
+
|
|
818
|
+
if saved_path != exe_path:
|
|
819
|
+
needs_update = True
|
|
820
|
+
path_changed = True
|
|
821
|
+
with open(path_file, 'w') as f:
|
|
822
|
+
f.write(exe_path)
|
|
823
|
+
|
|
753
824
|
if needs_update:
|
|
754
|
-
import sys
|
|
755
|
-
if getattr(sys, 'frozen', False):
|
|
756
|
-
exe_path = sys.executable
|
|
757
|
-
else:
|
|
758
|
-
exe_path = shutil.which('spolyrics') or sys.executable
|
|
759
825
|
if exe_path:
|
|
760
826
|
ps_script = f"$s=(New-Object -COM WScript.Shell).CreateShortcut('{shortcut_path}');$s.TargetPath='{exe_path}';$s.IconLocation='{icon_path}';$s.Save()"
|
|
761
827
|
subprocess.run(["powershell", "-Command", ps_script], creationflags=0x08000000)
|
|
762
828
|
except Exception as e:
|
|
763
829
|
logging.error("Failed to create shortcut", exc_info=e)
|
|
830
|
+
return path_changed
|
|
764
831
|
|
|
765
832
|
def setup_tray(app):
|
|
766
833
|
try:
|
|
@@ -789,27 +856,54 @@ def setup_tray(app):
|
|
|
789
856
|
logging.error("Failed to open tray icon image, using fallback", exc_info=e)
|
|
790
857
|
image = Image.new('RGB', (64, 64), color = (29, 185, 84))
|
|
791
858
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
859
|
+
def get_menu():
|
|
860
|
+
items = [
|
|
861
|
+
pystray.MenuItem("Shortcuts & Info", on_info),
|
|
862
|
+
pystray.MenuItem("Settings", on_settings),
|
|
863
|
+
pystray.MenuItem("Toggle Visibility", on_toggle_lyrics),
|
|
864
|
+
pystray.MenuItem("Quit", on_quit),
|
|
865
|
+
pystray.MenuItem(f"v{CURRENT_VERSION}", lambda: None, enabled=False)
|
|
866
|
+
]
|
|
867
|
+
if getattr(app, 'update_available', False):
|
|
868
|
+
items.insert(0, pystray.MenuItem("🔥 Update Available!", lambda: app.root.after(0, app.prompt_update)))
|
|
869
|
+
return pystray.Menu(*items)
|
|
870
|
+
|
|
871
|
+
icon = pystray.Icon("SpoLyrics", image, "SpoLyrics", menu=get_menu)
|
|
872
|
+
app.tray_icon = icon
|
|
800
873
|
threading.Thread(target=icon.run, daemon=True).start()
|
|
801
874
|
except Exception as e:
|
|
802
875
|
logging.error("Tray icon failed to load", exc_info=e)
|
|
803
876
|
|
|
877
|
+
def check_single_instance():
|
|
878
|
+
mutex_name = "Global\\SpoLyrics_Mutex"
|
|
879
|
+
mutex = ctypes.windll.kernel32.CreateMutexW(None, False, mutex_name)
|
|
880
|
+
last_error = ctypes.windll.kernel32.GetLastError()
|
|
881
|
+
|
|
882
|
+
if last_error == 183:
|
|
883
|
+
from tkinter import messagebox
|
|
884
|
+
import sys
|
|
885
|
+
temp_root = tk.Tk()
|
|
886
|
+
temp_root.withdraw()
|
|
887
|
+
temp_root.attributes('-topmost', True)
|
|
888
|
+
res = messagebox.askyesno("Already Running", "SpoLyrics is already running in the background.\n\nAre you sure you want to open another instance?", parent=temp_root)
|
|
889
|
+
temp_root.destroy()
|
|
890
|
+
if not res:
|
|
891
|
+
sys.exit(0)
|
|
892
|
+
return mutex
|
|
893
|
+
|
|
804
894
|
def start_app():
|
|
895
|
+
global _app_mutex
|
|
896
|
+
_app_mutex = check_single_instance()
|
|
897
|
+
|
|
805
898
|
try:
|
|
806
899
|
ctypes.windll.shcore.SetProcessDpiAwareness(1)
|
|
807
900
|
except Exception as e:
|
|
808
901
|
logging.error("Failed to set DPI awareness", exc_info=e)
|
|
902
|
+
logging.error("Failed to set DPI awareness", exc_info=e)
|
|
809
903
|
|
|
810
|
-
create_shortcut()
|
|
904
|
+
path_changed = create_shortcut()
|
|
811
905
|
config = load_config()
|
|
812
|
-
set_auto_start(config['auto_start'])
|
|
906
|
+
set_auto_start(config['auto_start'], force_update=path_changed)
|
|
813
907
|
|
|
814
908
|
app = MiniLyrics(config)
|
|
815
909
|
setup_tray(app)
|
|
@@ -2,7 +2,7 @@ from setuptools import setup
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name='spolyrics',
|
|
5
|
-
version='1.2.
|
|
5
|
+
version='1.2.2',
|
|
6
6
|
description='A minimalist, borderless, zero-delay synced lyrics miniplayer for Spotify on Windows.',
|
|
7
7
|
long_description=open('README.md', 'r', encoding='utf-8').read(),
|
|
8
8
|
long_description_content_type='text/markdown',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spolyrics
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
4
4
|
Summary: A minimalist, borderless, zero-delay synced lyrics miniplayer for Spotify on Windows.
|
|
5
5
|
Author: SpoLyrics Team
|
|
6
6
|
Requires-Python: >=3.8
|
|
@@ -25,7 +25,8 @@ Dynamic: summary
|
|
|
25
25
|
<h1 align="center">SpoLyrics</h1>
|
|
26
26
|
|
|
27
27
|
<p align="center">
|
|
28
|
-
<em>A minimalist, transparent, and zero-delay Spotify lyrics miniplayer.</em>
|
|
28
|
+
<em>A minimalist, transparent, and zero-delay Spotify lyrics miniplayer.</em><br>
|
|
29
|
+
<strong>🌐 <a href="https://paturrr.github.io/SpoLyrics/">Visit the Official Website</a></strong>
|
|
29
30
|
</p>
|
|
30
31
|
|
|
31
32
|
<p align="center">
|
|
@@ -75,6 +76,9 @@ I built this project to solve a specific complaint from my friends: they wanted
|
|
|
75
76
|
|
|
76
77
|
- **Advanced Customization (Settings)**
|
|
77
78
|
Change lyrics color using a modern real-time custom HSV color picker, adjust window transparency (opacity), and enable "Auto-Start with Windows". All configurations are permanently saved.
|
|
79
|
+
|
|
80
|
+
- **Auto Update Checker**
|
|
81
|
+
Automatically checks for the newest version on PyPI when you launch the app, offering a seamless one-click update and restart experience.
|
|
78
82
|
|
|
79
83
|
## Invisible Controls
|
|
80
84
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|