Files
2026-03-15 01:05:43 +08:00

235 lines
6.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import time
import tkinter as tk
import keyboard
import win32api
import win32con
import win32gui
POTPLAYER_CLASSES = {"PotPlayer64", "PotPlayer"}
APP_TITLE = "PotPlayer 同步器"
WINDOW_SIZE = "420x240"
PLAY_PAUSE_COOLDOWN = 0.25
WM_APPCOMMAND = 0x0319
APPCOMMAND_MEDIA_PLAY_PAUSE = 14
def safe_get_class_name(hwnd):
try:
return win32gui.GetClassName(hwnd)
except win32gui.error:
return ""
def safe_get_window_text(hwnd):
try:
return win32gui.GetWindowText(hwnd)
except win32gui.error:
return ""
def is_potplayer_window(hwnd):
return safe_get_class_name(hwnd) in POTPLAYER_CLASSES
def build_key_lparam(vk_code, is_keydown):
scan_code = win32api.MapVirtualKey(vk_code, 0)
is_extended = vk_code in {
win32con.VK_LEFT,
win32con.VK_RIGHT,
win32con.VK_UP,
win32con.VK_DOWN,
win32con.VK_CONTROL,
win32con.VK_RCONTROL,
win32con.VK_MENU,
win32con.VK_RMENU,
}
lparam = 1 | (scan_code << 16) | (int(is_extended) << 24)
if not is_keydown:
lparam |= (1 << 30) | (1 << 31)
return lparam
def get_potplayer_windows():
hwnds = []
def callback(hwnd, _extra):
if win32gui.IsWindowVisible(hwnd) and is_potplayer_window(hwnd):
hwnds.append(hwnd)
return True
win32gui.EnumWindows(callback, None)
return hwnds
class PotPlayerSyncApp:
def __init__(self):
self.root = tk.Tk()
self.root.title(APP_TITLE)
self.root.geometry(WINDOW_SIZE)
self.root.resizable(False, False)
self.root.attributes("-topmost", True)
self.root.protocol("WM_DELETE_WINDOW", self.on_close)
self.status_var = tk.StringVar(value="等待操作...")
self.last_play_pause_at = 0.0
self.hotkey_handles = []
self.build_ui()
self.register_hotkeys()
self.refresh_initial_status()
def build_ui(self):
tk.Label(
self.root,
text="PotPlayer 联动控制台",
font=("Microsoft YaHei", 13, "bold"),
pady=10,
).pack()
tips = (
"只在 PotPlayer 窗口或本工具窗口内操作时生效\n"
"\n"
"空格: 同步播放 / 暂停\n"
"Left / Right: 同步快退 / 快进\n"
"Shift + Left / Right: 同步微调进度\n"
"Ctrl + Left / Right: 同步大幅调整进度"
)
tk.Label(
self.root,
text=tips,
font=("Microsoft YaHei", 10),
justify="left",
fg="#333333",
padx=16,
pady=4,
).pack(anchor="w")
tk.Label(
self.root,
textvariable=self.status_var,
font=("Microsoft YaHei", 10, "bold"),
pady=10,
fg="#0066CC",
wraplength=380,
justify="left",
).pack()
def register_hotkeys(self):
self.hotkey_handles.extend(
[
keyboard.on_press_key("space", self.handle_space, suppress=False),
keyboard.on_press_key("left", self.handle_left, suppress=False),
keyboard.on_press_key("right", self.handle_right, suppress=False),
]
)
def refresh_initial_status(self):
count = len(get_potplayer_windows())
if count:
self.update_status(f"已检测到 {count} 个 PotPlayer 窗口,等待操作...")
else:
self.update_status("暂未检测到 PotPlayer 窗口。")
def update_status(self, message):
self.root.after(0, lambda: self.status_var.set(message))
def is_app_window(self, hwnd):
return safe_get_window_text(hwnd) == APP_TITLE
def get_target_windows(self):
foreground_hwnd = win32gui.GetForegroundWindow()
in_potplayer = is_potplayer_window(foreground_hwnd)
in_app = self.is_app_window(foreground_hwnd)
if not (in_potplayer or in_app):
return [], "请先聚焦 PotPlayer 窗口或同步器窗口。"
hwnds = get_potplayer_windows()
if not hwnds:
return [], "未检测到可同步的 PotPlayer 窗口。"
targets = [hwnd for hwnd in hwnds if hwnd != foreground_hwnd] if in_potplayer else hwnds
if not targets:
return [], "当前只有一个 PotPlayer 窗口,无需同步。"
return targets, ""
def post_play_pause(self, hwnd):
lparam = APPCOMMAND_MEDIA_PLAY_PAUSE << 16
win32gui.PostMessage(hwnd, WM_APPCOMMAND, 0, lparam)
def post_combo(self, hwnd, vk_codes):
for vk_code in vk_codes:
win32gui.PostMessage(hwnd, win32con.WM_KEYDOWN, vk_code, build_key_lparam(vk_code, True))
for vk_code in reversed(vk_codes):
win32gui.PostMessage(hwnd, win32con.WM_KEYUP, vk_code, build_key_lparam(vk_code, False))
def sync_action(self, action_name, sender):
target_hwnds, message = self.get_target_windows()
if not target_hwnds:
self.update_status(message)
return
for hwnd in target_hwnds:
try:
sender(hwnd)
except win32gui.error:
continue
self.update_status(f"已同步: {action_name} -> {len(target_hwnds)} 个窗口")
def handle_space(self, _event):
now = time.monotonic()
if now - self.last_play_pause_at < PLAY_PAUSE_COOLDOWN:
return
self.last_play_pause_at = now
self.sync_action("空格(播放/暂停)", self.post_play_pause)
def handle_left(self, _event):
if keyboard.is_pressed("shift"):
vk_codes = [win32con.VK_SHIFT, win32con.VK_LEFT]
action_name = "Shift + Left微调后退"
elif keyboard.is_pressed("ctrl"):
vk_codes = [win32con.VK_CONTROL, win32con.VK_LEFT]
action_name = "Ctrl + Left大步后退"
else:
vk_codes = [win32con.VK_LEFT]
action_name = "Left后退"
self.sync_action(action_name, lambda hwnd: self.post_combo(hwnd, vk_codes))
def handle_right(self, _event):
if keyboard.is_pressed("shift"):
vk_codes = [win32con.VK_SHIFT, win32con.VK_RIGHT]
action_name = "Shift + Right微调前进"
elif keyboard.is_pressed("ctrl"):
vk_codes = [win32con.VK_CONTROL, win32con.VK_RIGHT]
action_name = "Ctrl + Right大步前进"
else:
vk_codes = [win32con.VK_RIGHT]
action_name = "Right前进"
self.sync_action(action_name, lambda hwnd: self.post_combo(hwnd, vk_codes))
def on_close(self):
for handle in self.hotkey_handles:
keyboard.unhook(handle)
self.root.destroy()
def run(self):
self.root.mainloop()
def main():
app = PotPlayerSyncApp()
app.run()
if __name__ == "__main__":
main()