吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 324|回复: 7
上一主题 下一主题
收起左侧

[原创工具] 自制丑丑的本地播放器

[复制链接]
跳转到指定楼层
楼主
didi2019 发表于 2026-5-12 11:09 回帖奖励
闲的没事干手搓一个本地播放器,理论上支持大部分格式,就是长得丑,加入了在线歌词匹配有一些小bug,例如播放列表循环问题。页面相当简单(丑)(丑)(丑),还可能卡窗口和按钮的问题。播放列表保存功能加入刚兴趣的可以试试。由于bug问题和丑的问题不喜勿喷不接受差评
另外再发一个高仿的由于名字还没改,还请手下留情,高仿bug比较多,我就是个初学者都算不上的小学生,请大佬出手修复优化吧。播放列表不循环bug,播放滑块bug,按钮功能bug,不能最小化播放等
https://wwawd.lanzouw.com/b01bjer4wf
密码:52pj
丑丑的源码
[Python] 纯文本查看 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
DiDiPlayer v13 - 千千静听风格播放器
新增:歌词搜索结果选择、平滑滚动、频谱颜色渐变
"""

import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import pygame
import os
import threading
import json
import random
import urllib.request
import urllib.parse
import re
from pathlib import Path
from dataclasses import dataclass
from typing import List, Optional, Dict, Tuple
import struct
import wave
import math
import zlib
import hashlib
import time
import colorsys

# ===== 数据模型 =====

@dataclass
class Song:
    """歌曲"""
    path: str
    title: str = ""
    artist: str = ""
    album: str = ""
    duration: int = 0  # 秒

    def __post_init__(self):
        if not self.title:
            self.title = Path(self.path).stem
        self._load_metadata()

    def _load_metadata(self):
        """加载元数据"""
        try:
            if self.path.lower().endswith('.wav'):
                with wave.open(self.path, 'rb') as wf:
                    frames = wf.getnframes()
                    rate = wf.getframerate()
                    self.duration = int(frames / rate)
            elif self.path.lower().endswith(('.mp3', '.ogg', '.flac', '.m4a')):
                if pygame.mixer.get_init():
                    try:
                        snd = pygame.mixer.Sound(self.path)
                        self.duration = int(snd.get_length())
                    except:
                        pass
        except:
            pass


@dataclass
class LyricLine:
    """歌词行"""
    time: float  # 秒
    text: str


@dataclass
class LyricSearchResult:
    """歌词搜索结果"""
    title: str
    artist: str
    source: str  # 来源平台
    lyric_id: str
    lyric_url: Optional[str] = None


# ===== 歌词解析器 =====

class LyricParser:
    """多格式歌词解析器"""

    @staticmethod
    def parse(content: bytes, format_hint: str = None) -> List[LyricLine]:
        """自动识别格式并解析歌词"""
        text = None
        for encoding in ['utf-8', 'gbk', 'gb18030', 'utf-16']:
            try:
                text = content.decode(encoding)
                break
            except:
                continue

        if text is None:
            return []

        if format_hint == 'krc' or content[:4] == b'krc1':
            return LyricParser._parse_krc(content)
        elif format_hint == 'qrc' or '<Lyric_1 LyricType="1"' in text:
            return LyricParser._parse_qrc(text)
        elif format_hint == 'lrc' or '[' in text and ']' in text:
            return LyricParser._parse_lrc(text)
        else:
            return LyricParser._parse_txt(text)

    @staticmethod
    def _parse_lrc(content: str) -> List[LyricLine]:
        """解析 LRC 格式"""
        lines = []
        pattern = r'\[(\d{1,2}):(\d{1,2})(?:[.:](\d{1,3}))?\](.*)'

        for line in content.split('\n'):
            match = re.match(pattern, line.strip())
            if match:
                m, s, ms, text = match.groups()
                time = int(m) * 60 + int(s) + (int(ms or 0) / 100)
                if text.strip():
                    lines.append(LyricLine(time, text.strip()))

        return sorted(lines, key=lambda x: x.time)

    @staticmethod
    def _parse_krc(content: bytes) -> List[LyricLine]:
        """解析酷狗 KRC 格式"""
        lines = []

        try:
            if content[:4] == b'krc1':
                compressed = content[4:]
                try:
                    decompressed = zlib.decompress(compressed, -15)
                    text = decompressed.decode('utf-8', errors='ignore')
                except:
                    return []
            else:
                text = content.decode('utf-8', errors='ignore')

            pattern = r'\[(\d+),(\d+)\]([^\[]+)'
            for match in re.finditer(pattern, text):
                minute = int(match.group(1)) // 60000
                second = (int(match.group(1)) % 60000) // 1000
                ms = int(match.group(1)) % 1000
                time = minute * 60 + second + ms / 1000
                lyric_text = match.group(3)
                lyric_text = re.sub(r'<\d+,\d+,\d+>', '', lyric_text)
                if lyric_text.strip():
                    lines.append(LyricLine(time, lyric_text.strip()))
        except Exception as e:
            print(f"KRC 解析错误: {e}")

        return sorted(lines, key=lambda x: x.time)

    @staticmethod
    def _parse_qrc(content: str) -> List[LyricLine]:
        """解析QQ音乐 QRC 格式"""
        lines = []

        try:
            pattern = r'<!\[CDATA\[(.*?)\]\]>'
            match = re.search(pattern, content, re.DOTALL)

            if match:
                lyric_content = match.group(1)
                time_pattern = r'\((\d+),(\d+)\)([^(]*)'
                for m in re.finditer(time_pattern, lyric_content):
                    time = int(m.group(1)) / 1000
                    text = m.group(3)
                    if text.strip():
                        lines.append(LyricLine(time, text.strip()))
        except Exception as e:
            print(f"QRC 解析错误: {e}")

        return sorted(lines, key=lambda x: x.time)

    @staticmethod
    def _parse_txt(content: str) -> List[LyricLine]:
        """解析纯文本格式"""
        lines = []
        text_lines = [l.strip() for l in content.split('\n') if l.strip()]

        for i, text in enumerate(text_lines):
            lines.append(LyricLine(i * 5, text))

        return lines


# ===== 多源歌词搜索 =====

class MultiSourceLyricSearch:
    """多源歌词搜索引擎"""

    CACHE_DIR = Path(os.environ.get('LOCALAPPDATA', '.')) / 'DiDiPlayer' / 'lyrics'

    CHANNEL_STEREO = "stereo"
    CHANNEL_LEFT = "left"
    CHANNEL_RIGHT = "right"

    @classmethod
    def ensure_cache_dir(cls):
        cls.CACHE_DIR.mkdir(parents=True, exist_ok=True)

    @classmethod
    def search_all_sources(cls, title: str, artist: str = "") -> Tuple[List[LyricLine], str]:
        """从所有源搜索歌词,返回歌词和来源"""
        cls.ensure_cache_dir()

        cache_key = hashlib.md5(f"{title}_{artist}".encode()).hexdigest()
        cache_file = cls.CACHE_DIR / f"{cache_key}.lrc"

        if cache_file.exists():
            try:
                with open(cache_file, 'rb') as f:
                    return LyricParser.parse(f.read(), 'lrc'), "缓存"
            except:
                pass

        sources = [
            ("网易云", cls._search_netease),
            ("QQ音乐", cls._search_qq),
            ("酷狗", cls._search_kugou),
            ("酷我", cls._search_kuwo),
            ("咪咕", cls._search_migu),
        ]

        for source_name, search_func in sources:
            try:
                lyrics = search_func(title, artist)
                if lyrics:
                    try:
                        with open(cache_file, 'w', encoding='utf-8') as f:
                            for line in lyrics:
                                m, s = int(line.time // 60), int(line.time % 60)
                                ms = int((line.time % 1) * 100)
                                f.write(f"[{m:02d}:{s:02d}.{ms:02d}]{line.text}\n")
                    except:
                        pass
                    return lyrics, source_name
            except Exception as e:
                print(f"{source_name}搜索失败: {e}")
                continue

        return [], ""

    @classmethod
    def search_and_get_results(cls, title: str, artist: str = "") -> List[LyricSearchResult]:
        """搜索并返回所有结果供用户选择"""
        results = []
        for search_func in [cls._search_netease_results, cls._search_qq_results, cls._search_kugou_results]:
            try:
                r = search_func(title, artist)
                results.extend(r)
            except:
                pass
        return results[:15]  # 最多返回15个结果

    @classmethod
    def get_lyric_by_result(cls, result: LyricSearchResult) -> List[LyricLine]:
        """根据搜索结果获取歌词"""
        if result.source == "网易云":
            return cls._get_netease_lyric(result.lyric_id)
        elif result.source == "QQ音乐":
            return cls._get_qq_lyric(result.lyric_id, result.lyric_url or "")
        elif result.source == "酷狗":
            return cls._get_kugou_lyric(result.lyric_id)
        return []

    # 网易云
    @classmethod
    def _search_netease(cls, title: str, artist: str) -> List[LyricLine]:
        try:
            url = "https://music.163.com/api/search/get/?" + urllib.parse.urlencode({
                's': f"{title} {artist}".strip(), 'type': '1', 'limit': '1'
            })
            req = urllib.request.Request(url, headers={
                'User-Agent': 'Mozilla/5.0', 'Referer': 'https://music.163.com/'
            })
            with urllib.request.urlopen(req, timeout=8) as resp:
                data = json.loads(resp.read().decode('utf-8'))
            if data.get('code') != 200: return []
            songs = data.get('result', {}).get('songs', [])
            if not songs: return []
            return cls._get_netease_lyric(str(songs[0]['id']))
        except Exception as e:
            print(f"网易云搜索失败: {e}")
            return []

    @classmethod
    def _search_netease_results(cls, title: str, artist: str) -> List[LyricSearchResult]:
        results = []
        try:
            url = "https://music.163.com/api/search/get/?" + urllib.parse.urlencode({
                's': f"{title} {artist}".strip(), 'type': '1', 'limit': '5'
            })
            req = urllib.request.Request(url, headers={
                'User-Agent': 'Mozilla/5.0', 'Referer': 'https://music.163.com/'
            })
            with urllib.request.urlopen(req, timeout=8) as resp:
                data = json.loads(resp.read().decode('utf-8'))
            if data.get('code') != 200: return []
            for song in data.get('result', {}).get('songs', []):
                results.append(LyricSearchResult(
                    title=song.get('name', ''),
                    artist=', '.join([a.get('name', '') for a in song.get('artists', [])]),
                    source="网易云",
                    lyric_id=str(song['id'])
                ))
        except Exception as e:
            print(f"网易云搜索失败: {e}")
        return results

    @classmethod
    def _get_netease_lyric(cls, song_id: str) -> List[LyricLine]:
        try:
            url = f"https://music.163.com/api/song/lyric?id={song_id}&lv=1"
            req = urllib.request.Request(url, headers={
                'User-Agent': 'Mozilla/5.0', 'Referer': 'https://music.163.com/'
            })
            with urllib.request.urlopen(req, timeout=8) as resp:
                data = json.loads(resp.read().decode('utf-8'))
            if data.get('code') != 200: return []
            lrc = data.get('lrc', {}).get('lyric', '')
            return LyricParser._parse_lrc(lrc) if lrc else []
        except:
            return []

    # QQ音乐
    @classmethod
    def _search_qq(cls, title: str, artist: str) -> List[LyricLine]:
        try:
            results = cls._search_qq_results(title, artist)
            if results:
                return cls._get_qq_lyric(results[0].lyric_id, results[0].lyric_url or "")
        except:
            pass
        return []

    @classmethod
    def _search_qq_results(cls, title: str, artist: str) -> List[LyricSearchResult]:
        results = []
        try:
            url = "https://c.y.qq.com/soso/fcgi-bin/client_search_cp?" + urllib.parse.urlencode({
                'w': f"{title} {artist}".strip(), 'format': 'json', 'n': '5', 'p': '1'
            })
            req = urllib.request.Request(url, headers={
                'User-Agent': 'Mozilla/5.0', 'Referer': 'https://y.qq.com/'
            })
            with urllib.request.urlopen(req, timeout=8) as resp:
                data = json.loads(resp.read().decode('utf-8'))
            if data.get('code') != 0: return []
            for song in data.get('data', {}).get('song', {}).get('list', []):
                results.append(LyricSearchResult(
                    title=song.get('songname', ''),
                    artist=', '.join([s.get('name', '') for s in song.get('singer', [])]),
                    source="QQ音乐",
                    lyric_id=song.get('songmid', ''),
                    lyric_url=f"https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg?songmid={song.get('songmid', '')}"
                ))
        except:
            pass
        return results

    @classmethod
    def _get_qq_lyric(cls, song_mid: str, lyric_url: str) -> List[LyricLine]:
        try:
            import base64
            req = urllib.request.Request(lyric_url, headers={
                'User-Agent': 'Mozilla/5.0', 'Referer': 'https://y.qq.com/'
            })
            with urllib.request.urlopen(req, timeout=8) as resp:
                data = resp.read().decode('utf-8', errors='ignore')
            match = re.search(r'MusicJsonCallback\((.*)\)', data, re.DOTALL)
            if match:
                json_data = json.loads(match.group(1))
                lrc = json_data.get('lyric', '')
                if lrc:
                    lrc = base64.b64decode(lrc).decode('utf-8')
                    return LyricParser._parse_lrc(lrc)
        except:
            pass
        return []

    # 酷狗
    @classmethod
    def _search_kugou(cls, title: str, artist: str) -> List[LyricLine]:
        try:
            results = cls._search_kugou_results(title, artist)
            if results:
                return cls._get_kugou_lyric(results[0].lyric_id)
        except:
            pass
        return []

    @classmethod
    def _search_kugou_results(cls, title: str, artist: str) -> List[LyricSearchResult]:
        results = []
        try:
            url = "https://songsearch.kugou.com/song_search_v2?" + urllib.parse.urlencode({
                'keyword': f"{title} {artist}".strip(), 'page': '1', 'pagesize': '5', 'platform': 'WebFilter'
            })
            req = urllib.request.Request(url, headers={
                'User-Agent': 'Mozilla/5.0', 'Referer': 'https://www.kugou.com/'
            })
            with urllib.request.urlopen(req, timeout=8) as resp:
                data = json.loads(resp.read().decode('utf-8'))
            if data.get('status') != 1: return []
            for song in data.get('data', {}).get('lists', []):
                results.append(LyricSearchResult(
                    title=song.get('SongName', ''),
                    artist=song.get('SingerName', ''),
                    source="酷狗",
                    lyric_id=song.get('FileHash', '')
                ))
        except:
            pass
        return results

    @classmethod
    def _get_kugou_lyric(cls, file_hash: str) -> List[LyricLine]:
        try:
            url = f"https://www.kugou.com/yy/index.php?r=play/getdata&hash={file_hash}"
            req = urllib.request.Request(url, headers={
                'User-Agent': 'Mozilla/5.0', 'Referer': 'https://www.kugou.com/'
            })
            with urllib.request.urlopen(req, timeout=8) as resp:
                data = json.loads(resp.read().decode('utf-8'))
            if data.get('status') != 1: return []
            lyrics = data.get('data', {}).get('lyrics', '')
            return LyricParser._parse_lrc(lyrics) if lyrics else []
        except:
            return []

    # 酷我
    @classmethod
    def _search_kuwo(cls, title: str, artist: str) -> List[LyricLine]:
        try:
            url = "https://www.kuwo.cn/api/www/search/searchMusicBykeyWord?" + urllib.parse.urlencode({
                'key': f"{title} {artist}".strip(), 'pn': '1', 'rn': '1'
            })
            req = urllib.request.Request(url, headers={
                'User-Agent': 'Mozilla/5.0', 'Referer': 'https://www.kuwo.cn/search/list'
            })
            with urllib.request.urlopen(req, timeout=8) as resp:
                data = json.loads(resp.read().decode('utf-8'))
            if data.get('code') != 200: return []
            songs = data.get('data', {}).get('list', [])
            if not songs: return []
            return cls._get_kuwo_lyric(str(songs[0].get('rid', '')))
        except:
            return []

    @classmethod
    def _get_kuwo_lyric(cls, song_id: str) -> List[LyricLine]:
        try:
            url = f"https://www.kuwo.cn/api/v1/www/music/playInfo?mid={song_id}&type=lyric"
            req = urllib.request.Request(url, headers={
                'User-Agent': 'Mozilla/5.0', 'Referer': 'https://www.kuwo.cn/'
            })
            with urllib.request.urlopen(req, timeout=8) as resp:
                data = json.loads(resp.read().decode('utf-8'))
            if data.get('code') != 200: return []
            lrc_url = data.get('data', {}).get('lyric', '')
            if lrc_url:
                req2 = urllib.request.Request(lrc_url, headers={'User-Agent': 'Mozilla/5.0'})
                with urllib.request.urlopen(req2, timeout=8) as resp2:
                    return LyricParser._parse_lrc(resp2.read().decode('utf-8', errors='ignore'))
        except:
            pass
        return []

    # 咪咕
    @classmethod
    def _search_migu(cls, title: str, artist: str) -> List[LyricLine]:
        try:
            url = "https://m.music.migu.cn/migu/remoting/scr_search_tag?" + urllib.parse.urlencode({
                'keyword': f"{title} {artist}".strip(), 'type': '2', 'pg': '1', 'pz': '1'
            })
            req = urllib.request.Request(url, headers={
                'User-Agent': 'Mozilla/5.0 (Linux; Android)', 'Referer': 'https://m.music.migu.cn/'
            })
            with urllib.request.urlopen(req, timeout=8) as resp:
                data = json.loads(resp.read().decode('utf-8'))
            if data.get('success') != True: return []
            songs = data.get('musics', [])
            if not songs: return []
            return cls._get_migu_lyric(songs[0].get('copyrightId', ''))
        except:
            return []

    @classmethod
    def _get_migu_lyric(cls, copyright_id: str) -> List[LyricLine]:
        try:
            url = f"https://music.migu.cn/v3/api/listen-api/getLyric?copyrightId={copyright_id}"
            req = urllib.request.Request(url, headers={
                'User-Agent': 'Mozilla/5.0', 'Referer': 'https://music.migu.cn/'
            })
            with urllib.request.urlopen(req, timeout=8) as resp:
                data = json.loads(resp.read().decode('utf-8'))
            if data.get('returnCode') != '000000': return []
            lrc = data.get('lyric', '')
            return LyricParser._parse_lrc(lrc) if lrc else []
        except:
            return []


# ===== 窗口磁吸管理器 =====

class WindowSnapManager:
    SNAP_THRESHOLD = 25

    def __init__(self, main_window):
        self.main = main_window
        self.windows = {}
        self.snap_relations = {}
        self.snapping = False
        self.dragging = False

        self.main.bind('<Configure>', self._on_main_move, '+')
        self.main.bind('<ButtonPress-1>', lambda e: setattr(self, 'dragging', True), '+')
        self.main.bind('<ButtonRelease-1>', lambda e: setattr(self, 'dragging', False), '+')

    def register(self, name: str, window: tk.Toplevel):
        self.windows[name] = window
        window.bind('<Configure>', lambda e: self._on_window_move(name), '+')

    def _on_main_move(self, event):
        if self.snapping or not self.dragging:
            return

        main_x = self.main.winfo_x()
        main_y = self.main.winfo_y()
        main_w = self.main.winfo_width()
        main_h = self.main.winfo_height()

        for name, (target, edge) in list(self.snap_relations.items()):
            if target == 'main':
                window = self.windows.get(name)
                if window and window.winfo_viewable():
                    self.snapping = True
                    try:
                        wx, wy = window.winfo_x(), window.winfo_y()
                        if edge == 'right':
                            window.geometry(f"+{main_x + main_w}+{wy}")
                        elif edge == 'bottom':
                            window.geometry(f"+{wx}+{main_y + main_h}")
                    finally:
                        self.snapping = False

    def _on_window_move(self, name: str):
        if self.snapping or name not in self.windows:
            return

        moved = self.windows[name]
        new_x, new_y = moved.winfo_x(), moved.winfo_y()
        new_w, new_h = moved.winfo_width(), moved.winfo_height()

        main_x, main_y = self.main.winfo_x(), self.main.winfo_y()
        main_w, main_h = self.main.winfo_width(), self.main.winfo_height()

        snap_x, snap_y, snap_edge = None, None, None

        if abs(new_x - (main_x + main_w)) < self.SNAP_THRESHOLD:
            snap_x, snap_edge = main_x + main_w, 'right'
        elif abs((new_x + new_w) - main_x) < self.SNAP_THRESHOLD:
            snap_x, snap_edge = main_x - new_w, 'left'

        if abs(new_y - (main_y + main_h)) < self.SNAP_THRESHOLD:
            snap_y, snap_edge = main_y + main_h, snap_edge or 'bottom'
        elif abs((new_y + new_h) - main_y) < self.SNAP_THRESHOLD:
            snap_y, snap_edge = main_y - new_h, snap_edge or 'top'

        if snap_x is not None or snap_y is not None:
            self.snapping = True
            try:
                moved.geometry(f"+{snap_x or new_x}+{snap_y or new_y}")
                self.snap_relations[name] = ('main', snap_edge)
            finally:
                self.snapping = False
        else:
            self.snap_relations.pop(name, None)

    def snap_all_to_main(self):
        main_x, main_y = self.main.winfo_x(), self.main.winfo_y()
        main_w, main_h = self.main.winfo_width(), self.main.winfo_height()

        offset_y = 0
        for name, window in self.windows.items():
            if window.winfo_viewable():
                self.snapping = True
                try:
                    window.geometry(f"+{main_x + main_w}+{main_y + offset_y}")
                    self.snap_relations[name] = ('main', 'right')
                    offset_y += window.winfo_height()
                finally:
                    self.snapping = False


# ===== 频谱颜色管理器 =====

class SpectrumColorManager:
    """频谱颜色渐变管理器"""

    # 千千静听经典配色方案
    PRESETS = {
        'classic': ['#0066ff', '#00ccff', '#00ffcc', '#00ff66', '#66ff00'],
        'purple': ['#6600ff', '#9900ff', '#cc00ff', '#ff00ff', '#ff66ff'],
        'fire': ['#ff3300', '#ff6600', '#ff9900', '#ffcc00', '#ffff00'],
        'ocean': ['#003366', '#0066cc', '#0099ff', '#00ccff', '#66ffff'],
        'rainbow': ['#ff0000', '#ff9900', '#ffff00', '#00ff00', '#0099ff'],
    }

    def __init__(self, preset='classic'):
        self.preset = preset
        self.phase = 0.0
        self.colors = self.PRESETS.get(preset, self.PRESETS['classic'])

    def set_preset(self, preset: str):
        if preset in self.PRESETS:
            self.preset = preset
            self.colors = self.PRESETS[preset]

    def cycle_preset(self) -> str:
        presets = list(self.PRESETS.keys())
        idx = (presets.index(self.preset) + 1) % len(presets)
        self.set_preset(presets[idx])
        return self.preset

    def get_bar_color(self, bar_index: int, total_bars: int, height_ratio: float) -> str:
        """获取频谱条颜色 - 基于位置和高度渐变"""
        # 基于条位置的基础颜色插值
        pos_ratio = bar_index / max(1, total_bars - 1)
        color_idx = int(pos_ratio * (len(self.colors) - 1))
        color_idx = min(color_idx, len(self.colors) - 1)

        # 从基础颜色提取RGB
        base_color = self.colors[color_idx]
        r, g, b = self._hex_to_rgb(base_color)

        # 基于高度调整亮度
        brightness = 0.3 + height_ratio * 0.7
        r = int(r * brightness)
        g = int(g * brightness)
        b = int(b * brightness)

        # 添加相位偏移产生动态效果
        phase_shift = math.sin(self.phase + bar_index * 0.3) * 30
        r = max(0, min(255, r + int(phase_shift)))
        g = max(0, min(255, g + int(phase_shift * 0.5)))
        b = max(0, min(255, b - int(phase_shift * 0.3)))

        return f'#{r:02x}{g:02x}{b:02x}'

    def get_glow_color(self, bar_index: int) -> str:
        """获取发光颜色"""
        color_idx = bar_index % len(self.colors)
        return self.colors[color_idx]

    def advance_phase(self, delta: float = 0.1):
        """推进相位动画"""
        self.phase = (self.phase + delta) % (2 * math.pi)

    @staticmethod
    def _hex_to_rgb(hex_color: str) -> Tuple[int, int, int]:
        """十六进制转RGB"""
        hex_color = hex_color.lstrip('#')
        return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))


# ===== 频谱动画引擎 =====

class SpectrumEngine:
    """频谱动画引擎 - 模拟真实音频律动"""

    def __init__(self, num_bars: int = 24):
        self.num_bars = num_bars
        self.values = [0.0] * num_bars
        self.target_values = [0.0] * num_bars
        self.velocities = [0.0] * num_bars

        # 频谱物理参数
        self.gravity = 0.15
        self.dampening = 0.85
        self.attack_speed = 0.4
        self.decay_speed = 0.08

        # 节拍检测
        self.beat_phase = 0.0
        self.beat_intensity = 0.0
        self.last_beat_time = time.time()

        # 频率分布模拟(低频到高频)
        self.freq_weights = self._generate_freq_weights()

    def _generate_freq_weights(self) -> List[float]:
        """生成频率权重分布(模拟真实音频频谱)"""
        weights = []
        for i in range(self.num_bars):
            # 低频能量大,高频能量小,呈指数衰减
            weight = 1.0 - (i / self.num_bars) ** 0.7
            weights.append(weight * 0.5 + 0.5)
        return weights

    def update(self, is_playing: bool, position: float, duration: float):
        """更新频谱数据"""
        if not is_playing:
            # 暂停时衰减
            for i in range(self.num_bars):
                self.target_values[i] *= 0.9
        else:
            # 播放时生成频谱
            self._generate_spectrum(position, duration)

        # 应用物理动画
        self._apply_physics()

        # 更新节拍
        self._update_beat(is_playing, position)

    def _generate_spectrum(self, position: float, duration: float):
        """生成频谱数据(基于时间的模拟律动)"""
        progress = position / max(1, duration)

        for i in range(self.num_bars):
            # 基于时间的周期性变化
            time_factor = math.sin(position * (2 + i * 0.3) + i * 0.5)

            # 低频带变化慢,高频带变化快
            freq_factor = math.sin(position * (4 + i * 0.8) + i)

            # 随机性模拟真实音频
            noise = random.random() * 0.3 - 0.15

            # 综合计算目标值
            base = 0.2 + self.freq_weights[i] * 0.5
            variation = time_factor * 0.25 + freq_factor * 0.15 + noise

            # 添加节拍脉冲
            beat_pulse = self.beat_intensity * (1.0 - i / self.num_bars) * 0.4

            self.target_values[i] = max(0, min(1, base + variation + beat_pulse))

    def _apply_physics(self):
        """应用物理动画"""
        for i in range(self.num_bars):
            # 计算差值
            diff = self.target_values[i] - self.values[i]

            # 弹簧力
            spring_force = diff * self.attack_speed

            # 应用速度和重力
            self.velocities[i] += spring_force
            self.velocities[i] -= self.gravity * 0.1
            self.velocities[i] *= self.dampening

            # 更新值
            self.values[i] += self.velocities[i]
            self.values[i] = max(0, min(1, self.values[i]))

    def _update_beat(self, is_playing: bool, position: float):
        """更新节拍检测"""
        if is_playing:
            # 模拟 BPM 120 的节拍(每秒2拍)
            beat_interval = 0.5
            time_since_beat = (position % beat_interval) / beat_interval

            # 节拍峰值
            if time_since_beat < 0.1:
                self.beat_intensity = 1.0 - time_since_beat * 10
            else:
                self.beat_intensity *= 0.9
        else:
            self.beat_intensity *= 0.8

    def get_values(self) -> List[float]:
        """获取当前频谱值"""
        return self.values.copy()

    def get_beat_intensity(self) -> float:
        """获取节拍强度"""
        return self.beat_intensity


# ===== 音频引擎 =====

class AudioEngine:
    def __init__(self):
        self._sample_rate = 44100
        self._channel_mode = MultiSourceLyricSearch.CHANNEL_STEREO
        self._init_mixer()
        self._current_file = None
        self._paused = False
        self._volume = 0.7
        self._pause_pos = 0
        self._playing = False
        self._position_start_time = 0.0   # track real playback start (秒)
        self._position_start_pos = 0      # track start offset (毫秒)
        self._position_paused_at = 0.0    # how many seconds were accumulated before this pause

    def _init_mixer(self):
        try:
            pygame.mixer.quit()
            pygame.mixer.init(frequency=self._sample_rate, size=-16, channels=2, buffer=512)
            # 设置音乐结束事件
            pygame.mixer.music.set_endevent(pygame.USEREVENT + 1)
        except Exception as e:
            print(f"初始化混音器失败: {e}")

    def set_sample_rate(self, rate: int):
        if rate != self._sample_rate:
            self._sample_rate = rate
            was_playing = self.is_playing()
            was_paused = self.is_paused()
            pos = self.get_position()
            self._init_mixer()
            if self._current_file:
                self.load(self._current_file)
                self.set_position(pos)
                if was_playing: self.play()
                if was_paused: self.pause()

    def set_channel_mode(self, mode: str):
        self._channel_mode = mode
        self._apply_channel_mode()

    def _apply_channel_mode(self):
        vol = self._volume
        pygame.mixer.music.set_volume(vol)

    def get_channel_mode(self) -> str:
        return self._channel_mode

    def get_audio_info(self) -> dict:
        init_info = pygame.mixer.get_init()
        return {
            'frequency': init_info[0] if init_info else self._sample_rate,
            'channel_mode': self._channel_mode
        }

    def load(self, path: str) -> bool:
        try:
            self.stop()
            pygame.mixer.music.load(path)
            self._current_file = path
            self._pause_pos = 0
            self._playing = False
            return True
        except Exception as e:
            print(f"加载失败: {e}")
            return False

    def play(self):
        if self._current_file:
            if self._paused:
                # Resume: 保持 _position_paused_at 为暂停时的累计时间
                # get_position() 会返回: _position_paused_at + (now - resume_time)
                self._position_start_time = pygame.time.get_ticks() / 1000.0
                self._position_start_pos = 0  # 重置,因为已经累计到 _position_paused_at 里了
                pygame.mixer.music.unpause()
            else:
                pygame.mixer.music.play()
                # Fresh start
                self._position_start_time = pygame.time.get_ticks() / 1000.0
                self._position_start_pos = 0
                self._position_paused_at = 0.0
            self._paused = False
            self._playing = True
            self._apply_channel_mode()

    def pause(self):
        if self._playing and not self._paused:
            pygame.mixer.music.pause()
            # Capture accumulated time so far
            elapsed = (pygame.time.get_ticks() / 1000.0 - self._position_start_time) * 1000 + self._position_start_pos
            self._position_paused_at = elapsed / 1000.0
            self._pause_pos = self._position_paused_at
            self._paused = True

    def stop(self):
        pygame.mixer.music.stop()
        self._paused = False
        self._playing = False
        self._pause_pos = 0
        self._position_paused_at = 0.0
        self._position_start_time = 0.0
        self._position_start_pos = 0

    def set_volume(self, vol: float):
        self._volume = max(0, min(1, vol))
        self._apply_channel_mode()

    def get_volume(self) -> float:
        return self._volume

    def get_position(self) -> float:
        if self._paused:
            return self._pause_pos
        if not pygame.mixer.get_init():
            return 0.0
        try:
            busy = pygame.mixer.music.get_busy()
            if not busy:
                # Music finished naturally
                return self._pause_pos
            # 使用 pygame 内部计时器(毫秒)
            pos_ms = pygame.mixer.music.get_pos()
            if pos_ms < 0:
                return self._pause_pos
            # 返回累计暂停位置 + 当前播放位置
            return self._position_paused_at + pos_ms / 1000.0
        except Exception:
            return 0.0

    def set_position(self, pos: float):
        if self._current_file:
            was_playing = self._playing and not self._paused
            pygame.mixer.music.stop()
            pygame.mixer.music.play(start=pos)
            self._paused = False
            self._playing = True
            self._position_paused_at = pos
            self._position_start_time = pygame.time.get_ticks() / 1000.0
            self._position_start_pos = 0
            try:
                pygame.event.clear(pygame.USEREVENT + 1)
            except pygame.error:
                pass  # event subsystem not available
            if not was_playing:
                pygame.mixer.music.pause()
                self._paused = True
                self._pause_pos = pos

    def is_playing(self) -> bool:
        return self._playing and not self._paused

    def is_paused(self) -> bool:
        return self._paused

    def is_stopped(self) -> bool:
        """音乐是否已停止(非暂停、非播放)"""
        return not self._playing


# ===== 播放列表 =====

class PlaylistManager:
    MODE_ORDER = "order"
    MODE_RANDOM = "random"
    MODE_SINGLE = "single"
    MODE_LOOP = "loop"
    MODES = [MODE_ORDER, MODE_RANDOM, MODE_SINGLE, MODE_LOOP]

    def __init__(self):
        self.songs: List[Song] = []
        self.current_index = -1
        self._mode_index = 0
        self._random_history = []

    @property
    def mode(self) -> str:
        return self.MODES[self._mode_index]

    def cycle_mode(self) -> str:
        self._mode_index = (self._mode_index + 1) % len(self.MODES)
        if self.MODES[self._mode_index] == self.MODE_RANDOM:
            self._random_history = []
        return self.mode

    def add(self, song: Song):
        self.songs.append(song)

    def clear(self):
        self.songs.clear()
        self.current_index = -1
        self._random_history = []

    def next(self) -> Optional[Song]:
        if not self.songs: return None
        if self.mode == self.MODE_SINGLE:
            return self.songs[self.current_index] if 0 <= self.current_index < len(self.songs) else None
        elif self.mode == self.MODE_RANDOM:
            available = [i for i in range(len(self.songs)) if i not in self._random_history]
            if not available:
                self._random_history = []
                available = list(range(len(self.songs)))
            self.current_index = random.choice(available)
            self._random_history.append(self.current_index)
        elif self.mode == self.MODE_LOOP:
            self.current_index = (self.current_index + 1) % len(self.songs)
        else:
            self.current_index += 1
            if self.current_index >= len(self.songs): return None
        return self.songs[self.current_index] if 0 <= self.current_index < len(self.songs) else None

    def prev(self) -> Optional[Song]:
        if not self.songs: return None
        if self.mode == self.MODE_SINGLE:
            return self.songs[self.current_index] if 0 <= self.current_index < len(self.songs) else None
        elif self.mode == self.MODE_RANDOM:
            if len(self._random_history) > 1:
                self._random_history.pop()
                self.current_index = self._random_history[-1]
            else:
                self.current_index = random.randint(0, len(self.songs) - 1)
        else:
            self.current_index = (self.current_index - 1) % len(self.songs)
        return self.songs[self.current_index] if 0 <= self.current_index < len(self.songs) else None

    def get_current(self) -> Optional[Song]:
        return self.songs[self.current_index] if 0 <= self.current_index < len(self.songs) else None


# ===== 主应用 =====

class DiDiPlayerApp:
    APP_NAME = "迪52o"
    VERSION = "v14.0"

    COLORS = {
        'bg_dark': '#1a0033', 'bg_mid': '#2d004d', 'bg_light': '#4a0080',
        'accent': '#00cc66', 'accent_dark': '#009944',
        'text': '#ffffff', 'text_dim': '#b388ff', 'text_gold': '#ffd700',
        'progress_bg': '#333366', 'progress_fill': '#00cc66',
        'spectrum': '#4488ff', 'mode_active': '#ff6600',
    }

    # 播放列表保存路径
    PLAYLIST_DIR = Path(os.environ.get('LOCALAPPDATA', '.')) / 'DiDiPlayer'
    PLAYLIST_FILE = PLAYLIST_DIR / 'playlist.json'
    CONFIG_FILE = PLAYLIST_DIR / 'config.json'

    MODE_INFO = {
        PlaylistManager.MODE_ORDER: ("&#10145;&#65039;", "顺序"),
        PlaylistManager.MODE_RANDOM: ("&#128256;", "随机"),
        PlaylistManager.MODE_SINGLE: ("&#128258;", "单曲"),
        PlaylistManager.MODE_LOOP: ("&#128257;", "循环"),
    }

    CHANNEL_INFO = {
        MultiSourceLyricSearch.CHANNEL_STEREO: ("&#128266;", "立体声"),
        MultiSourceLyricSearch.CHANNEL_LEFT: ("&#9664;", "左声道"),
        MultiSourceLyricSearch.CHANNEL_RIGHT: ("&#9654;", "右声道"),
    }

    def __init__(self):
        # 状态文件
        self.STATE_FILE = self.PLAYLIST_DIR / "state.json"

        # 创建配置目录
        self.PLAYLIST_DIR.mkdir(parents=True, exist_ok=True)

        # 加载配置
        self.config = self._load_config()

        self.root = tk.Tk()
        self.root.title(f"{self.APP_NAME} {self.VERSION}")
        self.root.configure(bg=self.COLORS['bg_dark'])
        self.root.geometry("480x420")

        # 背景图片
        self.bg_image = None
        self.bg_label = None
        self._apply_background()

        # 透明度(Windows only)
        try:
            self.root.attributes('-alpha', self.config.get('opacity', 1.0))
        except:
            pass

        # 退出时保存状态
        self.root.protocol("WM_DELETE_WINDOW", self._quit_and_save)

        # 进度条拖拽状态
        self._dragging = False

        self.audio = AudioEngine()
        self.playlist = PlaylistManager()
        self.snap_manager = WindowSnapManager(self.root)
        self.spectrum_engine = SpectrumEngine(num_bars=24)
        self.color_manager = SpectrumColorManager(preset='classic')

        self._lyrics: List[LyricLine] = []
        self._lyric_index = -1
        self._lyric_search_results: List[LyricSearchResult] = []
        self._smooth_scroll_offset = 0.0
        self._lyrics_loading = False  # 歌词加载中标志

        self._build_ui()
        self._restore_state()  # 恢复上次退出状态
        self._update_loop()

    def _build_ui(self):
        main = tk.Frame(self.root, bg=self.COLORS['bg_dark'])
        main.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        # 歌曲信息
        self.title_label = tk.Label(main, text="欢迎使用 迪52o", font=("Microsoft YaHei", 14, "bold"),
            fg=self.COLORS['text_gold'], bg=self.COLORS['bg_dark'])
        self.title_label.pack(fill=tk.X)

        self.time_label = tk.Label(main, text="00:00 / 00:00", font=("Consolas", 11),
            fg=self.COLORS['text_dim'], bg=self.COLORS['bg_dark'])
        self.time_label.pack(fill=tk.X)

        # 频谱画布
        self.spectrum_canvas = tk.Canvas(main, height=60, bg=self.COLORS['bg_mid'], highlightthickness=0)
        self.spectrum_canvas.pack(fill=tk.X, pady=5)

        # 频谱配色按钮
        spectrum_ctrl = tk.Frame(main, bg=self.COLORS['bg_dark'])
        spectrum_ctrl.pack(fill=tk.X)
        tk.Label(spectrum_ctrl, text="配色:", bg=self.COLORS['bg_dark'], fg=self.COLORS['text_dim']).pack(side=tk.LEFT)
        self.color_btn = tk.Button(spectrum_ctrl, text="&#127912;", command=self._cycle_color,
            bg=self.COLORS['bg_light'], fg=self.COLORS['text'], bd=0, width=3, cursor='hand2')
        self.color_btn.pack(side=tk.LEFT, padx=5)

        # 进度条(固定宽度,确保 winfo_reqwidth 有返回值)
        self.progress_canvas = tk.Canvas(main, height=15, width=460, bg=self.COLORS['progress_bg'], highlightthickness=0)
        self.progress_canvas.pack(fill=tk.X)
        # 拖拽进度条支持
        self.progress_canvas.bind('<Button-1>', self._seek_start)
        self.progress_canvas.bind('<B1-Motion>', self._seek_drag)
        self.progress_canvas.bind('<ButtonRelease-1>', self._seek_end)

        # 控制按钮
        ctrl = tk.Frame(main, bg=self.COLORS['bg_dark'])
        ctrl.pack(fill=tk.X, pady=10)

        self.btn_prev = tk.Button(ctrl, text="&#9198;", command=self._prev, **self._btn_style())
        self.btn_prev.pack(side=tk.LEFT, padx=2)

        self.btn_play = tk.Button(ctrl, text="&#9654;", command=self._toggle, bg=self.COLORS['accent'], width=4)
        self.btn_play.pack(side=tk.LEFT, padx=2)

        self.btn_next = tk.Button(ctrl, text="&#9197;", command=self._next, **self._btn_style())
        self.btn_next.pack(side=tk.LEFT, padx=2)

        self.btn_mode = tk.Button(ctrl, text="&#10145;&#65039;", command=self._cycle_mode, **self._btn_style())
        self.btn_mode.pack(side=tk.LEFT, padx=10)

        self.btn_channel = tk.Button(ctrl, text="&#128266;", command=self._cycle_channel, **self._btn_style())
        self.btn_channel.pack(side=tk.LEFT, padx=2)

        self.volume_scale = tk.Scale(ctrl, from_=0, to=100, orient=tk.HORIZONTAL, length=80,
            command=self._volume, bg=self.COLORS['bg_dark'], fg=self.COLORS['text'],
            troughcolor=self.COLORS['progress_bg'], highlightthickness=0)
        self.volume_scale.set(70)
        self.volume_scale.pack(side=tk.LEFT, padx=5)

        self.btn_lyric = tk.Button(ctrl, text="&#127908;", command=self._toggle_lyric, **self._btn_style())
        self.btn_lyric.pack(side=tk.RIGHT, padx=2)

        self.btn_list = tk.Button(ctrl, text="&#128203;", command=self._toggle_list, **self._btn_style())
        self.btn_list.pack(side=tk.RIGHT, padx=2)

        self.btn_open = tk.Button(ctrl, text="&#128194;", command=self._open, **self._btn_style())
        self.btn_open.pack(side=tk.RIGHT, padx=2)

        # 状态栏
        self.status_label = tk.Label(main, text="状态: 就绪 | 立体声 | 44.1kHz",
            font=("Microsoft YaHei", 9), fg=self.COLORS['text_dim'], bg=self.COLORS['bg_dark'])
        self.status_label.pack(side=tk.BOTTOM, fill=tk.X)

        self._build_list_window()
        self._build_lyric_window()
        self._build_search_result_window()

    def _btn_style(self):
        return {'bg': self.COLORS['bg_light'], 'fg': self.COLORS['text'],
            'bd': 0, 'width': 3, 'height': 1, 'cursor': 'hand2'}

    def _build_list_window(self):
        self.list_win = tk.Toplevel(self.root)
        self.list_win.title("播放列表")
        self.list_win.geometry("420x280+500+0")
        self.list_win.configure(bg=self.COLORS['bg_dark'])
        self.list_win.withdraw()
        self.snap_manager.register('list', self.list_win)

        # 工具栏
        toolbar = tk.Frame(self.list_win, bg=self.COLORS['bg_mid'])
        toolbar.pack(fill=tk.X)

        tk.Button(toolbar, text="&#10133;", command=self._open, **self._btn_style()).pack(side=tk.LEFT, padx=2)
        tk.Button(toolbar, text="&#10134;", command=self._remove, **self._btn_style()).pack(side=tk.LEFT, padx=2)
        tk.Button(toolbar, text="&#128465;", command=self._clear, **self._btn_style()).pack(side=tk.LEFT, padx=2)
        tk.Button(toolbar, text="&#128190;", command=self._save_playlist, **self._btn_style()).pack(side=tk.LEFT, padx=2)
        tk.Button(toolbar, text="&#128194;", command=self._load_playlist, **self._btn_style()).pack(side=tk.LEFT, padx=2)

        tk.Label(toolbar, text="采样率:", bg=self.COLORS['bg_mid'], fg=self.COLORS['text_dim']).pack(side=tk.LEFT, padx=(10, 2))

        self.sample_var = tk.StringVar(value="44.1kHz")
        ttk.Combobox(toolbar, textvariable=self.sample_var, values=["44.1kHz", "48kHz", "96kHz"],
            width=7, state='readonly').pack(side=tk.LEFT)
        self.sample_var.trace('w', self._sample_change)

        self.listbox = tk.Listbox(self.list_win, bg=self.COLORS['bg_mid'], fg=self.COLORS['text'],
            selectbackground=self.COLORS['accent'], font=("Microsoft YaHei", 10), bd=0, highlightthickness=0)
        self.listbox.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
        self.listbox.bind('<Double-Button-1>', self._play_selected)

    def _build_lyric_window(self):
        self.lyric_win = tk.Toplevel(self.root)
        self.lyric_win.title("歌词")
        self.lyric_win.geometry("420x260+500+300")
        self.lyric_win.configure(bg=self.COLORS['bg_dark'])
        self.lyric_win.withdraw()
        self.snap_manager.register('lyric', self.lyric_win)

        # 搜索框
        search = tk.Frame(self.lyric_win, bg=self.COLORS['bg_mid'])
        search.pack(fill=tk.X, padx=5, pady=5)

        tk.Label(search, text="搜索:", bg=self.COLORS['bg_mid'], fg=self.COLORS['text_dim']).pack(side=tk.LEFT, padx=5)

        self.search_entry = tk.Entry(search, bg=self.COLORS['bg_light'], fg=self.COLORS['text'],
            font=("Microsoft YaHei", 10), bd=0)
        self.search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=2)
        self.search_entry.bind('<Return>', lambda e: self._search_lyric())

        tk.Button(search, text="&#128269;", command=self._search_lyric, **self._btn_style()).pack(side=tk.LEFT, padx=2)
        tk.Button(search, text="&#128279;", command=self._auto_lyric, **self._btn_style()).pack(side=tk.LEFT, padx=2)

        # 歌词显示
        self.lyric_text = tk.Text(self.lyric_win, bg=self.COLORS['bg_mid'], fg=self.COLORS['text'],
            font=("Microsoft YaHei", 12), bd=0, highlightthickness=0, wrap=tk.WORD)
        self.lyric_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
        self.lyric_text.config(state=tk.DISABLED)

        self.lyric_source = tk.Label(self.lyric_win, text="", font=("Microsoft YaHei", 8),
            fg=self.COLORS['text_dim'], bg=self.COLORS['bg_dark'])
        self.lyric_source.pack(side=tk.BOTTOM)

    def _build_search_result_window(self):
        """歌词搜索结果窗口"""
        self.search_win = tk.Toplevel(self.root)
        self.search_win.title("选择歌词")
        self.search_win.geometry("500x350+550+100")
        self.search_win.configure(bg=self.COLORS['bg_dark'])
        self.search_win.withdraw()

        tk.Label(self.search_win, text="双击选择最匹配的歌词:",
            bg=self.COLORS['bg_dark'], fg=self.COLORS['text_dim'],
            font=("Microsoft YaHei", 10)).pack(pady=5)

        # 结果列表
        columns = ('title', 'artist', 'source')
        self.result_tree = ttk.Treeview(self.search_win, columns=columns, show='headings', height=12)
        self.result_tree.heading('title', text='歌曲名')
        self.result_tree.heading('artist', text='歌手')
        self.result_tree.heading('source', text='来源')
        self.result_tree.column('title', width=200)
        self.result_tree.column('artist', width=150)
        self.result_tree.column('source', width=100)
        self.result_tree.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
        self.result_tree.bind('<Double-Button-1>', self._select_lyric_result)

        # 配置样式
        style = ttk.Style()
        style.theme_use('clam')
        style.configure("Treeview", background=self.COLORS['bg_mid'],
            foreground=self.COLORS['text'], fieldbackground=self.COLORS['bg_mid'])
        style.configure("Treeview.Heading", background=self.COLORS['bg_light'],
            foreground=self.COLORS['text'])
        style.map("Treeview", background=[('selected', self.COLORS['accent'])])

    def _cycle_mode(self):
        mode = self.playlist.cycle_mode()
        self.btn_mode.config(text=self.MODE_INFO[mode][0])
        self.status_label.config(text=self._status_text())

    def _cycle_channel(self):
        modes = [MultiSourceLyricSearch.CHANNEL_STEREO,
                 MultiSourceLyricSearch.CHANNEL_LEFT,
                 MultiSourceLyricSearch.CHANNEL_RIGHT]
        current = self.audio.get_channel_mode()
        next_mode = modes[(modes.index(current) + 1) % len(modes)]
        self.audio.set_channel_mode(next_mode)
        self.btn_channel.config(text=self.CHANNEL_INFO[next_mode][0])
        self.status_label.config(text=self._status_text())

    def _cycle_color(self):
        preset = self.color_manager.cycle_preset()
        self.color_btn.config(text=f"&#127912;")

    def _sample_change(self, *args):
        rate_map = {"44.1kHz": 44100, "48kHz": 48000, "96kHz": 96000}
        self.audio.set_sample_rate(rate_map.get(self.sample_var.get(), 44100))
        self.status_label.config(text=self._status_text())

    def _status_text(self) -> str:
        info = self.audio.get_audio_info()
        sr = info['frequency']
        sr_text = f"{sr/1000:.1f}kHz" if sr < 1000 else f"{sr//1000}kHz"
        ch_text = self.CHANNEL_INFO.get(info['channel_mode'], ("", "立体声"))[1]
        mode_text = self.MODE_INFO[self.playlist.mode][1]
        color_preset = self.color_manager.preset
        return f"状态: {mode_text} | {ch_text} | {sr_text} | 配色:{color_preset}"

    def _toggle(self):
        if self.audio.is_playing():
            self.audio.pause()
            self.btn_play.config(text="&#9654;")
        elif self.audio.is_paused():
            self.audio.play()
            self.btn_play.config(text="&#9208;")
        elif self.playlist.get_current():
            self._play(self.playlist.get_current())

    def _play(self, song: Song):
        if self.audio.load(song.path):
            self.audio.play()
            self.btn_play.config(text="&#9208;")
            self.title_label.config(text=f"{song.title} - {song.artist}" if song.artist else song.title)
            # 确保时长正确
            if song.duration <= 0:
                song._load_metadata()
            self._load_lyric(song)

    def _prev(self):
        song = self.playlist.prev()
        if song: self._play(song)

    def _next(self):
        song = self.playlist.next()
        if song: self._play(song)
        else:
            self.audio.stop()
            self.btn_play.config(text="&#9654;")

    def _open(self):
        files = filedialog.askopenfilenames(filetypes=[("音乐", "*.mp3 *.wav *.ogg *.flac *.m4a")])
        if files:
            for f in files:
                song = Song(f)
                self.playlist.add(song)
                self.listbox.insert(tk.END, f"{song.title} - {song.artist}" if song.artist else song.title)

    def _remove(self):
        if self.listbox.curselection():
            idx = self.listbox.curselection()[0]
            self.listbox.delete(idx)
            del self.playlist.songs[idx]

    def _clear(self):
        self.playlist.clear()
        self.listbox.delete(0, tk.END)

    def _play_selected(self, event):
        if self.listbox.curselection():
            self.playlist.current_index = self.listbox.curselection()[0]
            self._play(self.playlist.get_current())

    def _toggle_list(self):
        if self.list_win.winfo_viewable():
            self.list_win.withdraw()
        else:
            self.list_win.deiconify()

    def _toggle_lyric(self):
        if self.lyric_win.winfo_viewable():
            self.lyric_win.withdraw()
        else:
            self.lyric_win.deiconify()

    def _seek(self, x):
        song = self.playlist.get_current()
        if song and song.duration > 0:
            # 限制 x 在进度条范围内
            w = self.progress_canvas.winfo_width()
            if w <= 0: return
            x = max(0, min(x, w))
            pos = (x / w) * song.duration
            self.audio.set_position(max(0, pos))

    def _volume(self, val):
        self.audio.set_volume(float(val) / 100)

    def _load_lyric(self, song: Song):
        self._lyrics = []
        self._lyric_index = -1
        self._lyrics_loading = True  # 标记正在加载

        # 尝试本地歌词
        found_local = False
        for ext in ['.lrc', '.krc', '.qrc', '.txt']:
            lrc_path = Path(song.path).with_suffix(ext)
            if lrc_path.exists():
                try:
                    with open(lrc_path, 'rb') as f:
                        self._lyrics = LyricParser.parse(f.read(), ext[1:])
                    if self._lyrics:
                        self.lyric_source.config(text=f"来源: 本地{ext}")
                        self._lyrics_loading = False
                        found_local = True
                        break
                except:
                    continue

        # 更新显示
        self._update_lyric_display()

        # 填充搜索框
        self.search_entry.delete(0, tk.END)
        self.search_entry.insert(0, f"{song.title} {song.artist}".strip())

        # 如果没有本地歌词,自动在线搜索
        if not found_local and song.title:
            threading.Thread(target=self._auto_search_lyric, args=(song,), daemon=True).start()

    def _update_lyric_display(self):
        """更新歌词显示"""
        if not self._lyrics:
            return
        self._lyrics_loading = False
        self.lyric_text.config(state=tk.NORMAL)
        self.lyric_text.delete(1.0, tk.END)
        for line in self._lyrics:
            self.lyric_text.insert(tk.END, line.text + '\n')
        self.lyric_text.config(state=tk.DISABLED)

    def _search_lyric(self):
        """搜索歌词并显示结果"""
        query = self.search_entry.get().strip()
        if not query:
            return
        threading.Thread(target=self._do_search_and_show, args=(query,), daemon=True).start()

    def _auto_lyric(self):
        song = self.playlist.get_current()
        if song:
            self._do_search_and_show(f"{song.title} {song.artist}".strip())

    def _do_search_and_show(self, query: str):
        """搜索并显示结果列表"""
        try:
            results = MultiSourceLyricSearch.search_and_get_results(query, "")
            if results:
                self._lyric_search_results = results
                # 在主线程更新UI
                self.root.after(0, self._show_search_results)
            else:
                self.root.after(0, lambda: messagebox.showinfo("提示", "未找到匹配的歌词"))
        except Exception as e:
            print(f"歌词搜索失败: {e}")

    def _show_search_results(self):
        """显示搜索结果窗口"""
        # 清空旧结果
        for item in self.result_tree.get_children():
            self.result_tree.delete(item)

        # 添加新结果
        for i, result in enumerate(self._lyric_search_results):
            self.result_tree.insert('', tk.END, iid=str(i), values=(
                result.title,
                result.artist,
                result.source
            ))

        # 显示窗口
        self.search_win.deiconify()
        self.search_win.lift()

    def _select_lyric_result(self, event):
        """选择搜索结果"""
        selection = self.result_tree.selection()
        if not selection:
            return

        idx = int(selection[0])
        if 0 <= idx < len(self._lyric_search_results):
            result = self._lyric_search_results[idx]
            threading.Thread(target=self._load_selected_lyric, args=(result,), daemon=True).start()
            self.search_win.withdraw()

    def _load_selected_lyric(self, result: LyricSearchResult):
        """加载选中的歌词"""
        try:
            lyrics = MultiSourceLyricSearch.get_lyric_by_result(result)
            if lyrics:
                self._lyrics = lyrics
                self.root.after(0, self._update_lyric_display)
                self.root.after(0, lambda: self.lyric_source.config(text=f"来源: {result.source} - {result.title}"))
        except Exception as e:
            print(f"加载歌词失败: {e}")

    def _auto_search_lyric(self, song: Song):
        """自动在线搜索歌词"""
        try:
            query = f"{song.title} {song.artist}".strip()
            lyrics, source = MultiSourceLyricSearch.search_all_sources(query, "")
            if lyrics:
                self._lyrics = lyrics
                self.root.after(0, self._update_lyric_display)
                self.root.after(0, lambda: self.lyric_source.config(text=f"来源: {source}"))
        except Exception as e:
            print(f"自动搜索歌词失败: {e}")

    def _update_loop(self):
        try:
            self._update_progress()
            self._update_spectrum()
            self._update_lyric()
            self._check_end()
        except tk.TclError:
            pass  # 窗口销毁后忽略
        except Exception as e:
            print(f"更新循环错误: {e}")
        try:
            self.root.after(50, self._update_loop)
        except tk.TclError:
            pass  # 窗口已销毁

    def _update_progress(self):
        # 如果正在拖拽,跳过更新
        if self._dragging:
            return

        song = self.playlist.get_current()
        if not song: return

        # 安全检查:pygame mixer 是否已初始化
        if not pygame.mixer.get_init():
            return

        pos = self.audio.get_position()
        # 动态获取时长(如果元数据加载失败)
        dur = song.duration if song.duration > 0 else 300  # 默认5分钟

        # 确保进度在有效范围内
        if pos < 0: pos = 0
        if pos > dur: pos = dur

        try:
            self.time_label.config(text=f"{int(pos//60):02d}:{int(pos%60):02d} / {int(dur//60):02d}:{int(dur%60):02d}")
        except tk.TclError:
            return

        w = self.progress_canvas.winfo_reqwidth()
        if w <= 0: w = 460
        fill = int(w * min(1, pos / dur))

        try:
            self.progress_canvas.delete('all')
            # 第1层:背景轨道
            self.progress_canvas.create_rectangle(
                0, 0, w, 15,
                fill=self.COLORS.get('progress_bg', '#333366'),
                outline=''
            )
            # 第2层:前景进度
            self.progress_canvas.create_rectangle(
                0, 0, fill, 15,
                fill=self.COLORS['progress_fill'],
                outline=''
            )
            # 第3层:圆点指示器
            self.progress_canvas.create_oval(
                fill - 6, 1, fill + 6, 14,
                fill=self.COLORS['text'],
                outline=self.COLORS['accent']
            )
        except tk.TclError:
            return


    def _update_spectrum(self):
        """更新频谱动画"""
        self.spectrum_canvas.delete('all')
        w = self.spectrum_canvas.winfo_width()
        h = self.spectrum_canvas.winfo_height()
        if w <= 0 or h <= 0: return

        # 安全检查:pygame mixer 是否已初始化
        if not pygame.mixer.get_init():
            return

        # 获取当前位置和时长
        song = self.playlist.get_current()
        pos = self.audio.get_position() if song else 0
        dur = song.duration if song else 1

        # 更新频谱引擎
        self.spectrum_engine.update(self.audio.is_playing(), pos, dur)
        values = self.spectrum_engine.get_values()

        # 推进颜色相位
        self.color_manager.advance_phase(0.05)

        # 绘制频谱条
        num_bars = len(values)
        bar_w = (w - 30) // num_bars
        gap = 2

        for i, val in enumerate(values):
            bar_h = int(val * h * 0.9)
            x1 = i * (bar_w + gap) + 15
            y1 = h - bar_h

            # 获取渐变颜色
            color = self.color_manager.get_bar_color(i, num_bars, val)
            glow_color = self.color_manager.get_glow_color(i)

            # 绘制发光效果
            if bar_h > 5:
                self.spectrum_canvas.create_rectangle(
                    x1-1, y1-2, x1+bar_w+1, h,
                    fill=glow_color, outline='', stipple='gray50'
                )

            # 绘制主频谱条
            self.spectrum_canvas.create_rectangle(
                x1, y1, x1+bar_w, h,
                fill=color, outline=''
            )

            # 绘制顶部高亮
            if bar_h > 3:
                self.spectrum_canvas.create_rectangle(
                    x1, y1, x1+bar_w, y1+3,
                    fill=self.COLORS['text'], outline=''
                )

    def _update_lyric(self):
        """更新歌词滚动"""
        if not self._lyrics:
            # 只在第一次显示提示,避免重复刷新
            if not self._lyrics_loading and hasattr(self, 'lyric_text'):
                self._lyrics_loading = True
                self.lyric_text.config(state=tk.NORMAL)
                self.lyric_text.delete(1.0, tk.END)
                self.lyric_text.insert(tk.END, "暂无歌词\n\n点击 &#128269; 搜索歌词\n或 点击 &#128279; 自动匹配")
                self.lyric_text.config(state=tk.DISABLED)
            return

        self._lyrics_loading = False  # 有歌词,取消加载标志

        pos = self.audio.get_position()
        new_idx = -1
        for i, line in enumerate(self._lyrics):
            if line.time <= pos: new_idx = i
            else: break

        if new_idx != self._lyric_index:
            self._lyric_index = new_idx
            self.lyric_text.config(state=tk.NORMAL)
            self.lyric_text.tag_remove('hl', '1.0', tk.END)
            if new_idx >= 0:
                self.lyric_text.tag_add('hl', f"{new_idx+1}.0", f"{new_idx+1}.end")
                # 平滑滚动到当前行
                self._smooth_scroll_to_line(new_idx + 1)
            self.lyric_text.tag_configure('hl', foreground=self.COLORS['text_gold'],
                font=("Microsoft YaHei", 14, "bold"))
            self.lyric_text.config(state=tk.DISABLED)

    def _smooth_scroll_to_line(self, line_num):
        """平滑滚动到指定行"""
        # 计算目标位置
        total_lines = len(self._lyrics)
        visible_lines = 10  # 可见行数估算
        target = max(0, line_num - visible_lines // 2) / max(1, total_lines)

        # 直接滚动(tkinter没有真正的平滑滚动API)
        self.lyric_text.yview_moveto(target)

    def _check_end(self):
        """检查音乐是否播放结束,触发下一曲或单曲循环

        不使用 pygame.event.get()(需要 video system),改为纯 get_busy() + 位置检测。
        """
        # 安全检查:pygame mixer 是否已初始化
        if not pygame.mixer.get_init():
            return

        song = self.playlist.get_current()
        if not song:
            return

        # 方法1:get_busy() 检测自然结束
        # 注意:pause() 后 get_busy() 会返回 False,所以必须排除暂停状态
        busy = pygame.mixer.music.get_busy()
        if not busy and self.audio._playing and not self.audio._paused:
            # 音乐自然播放结束(且不是暂停状态),触发下一曲
            self._on_music_end()
            return

        # 方法2:位置超出时长(兜底,适用于 get_busy 漏检的情况)
        if self.audio.is_playing():
            pos = self.audio.get_position()
            dur = song.duration if song.duration > 0 else 0
            if dur > 0 and pos >= dur - 0.5:
                self._on_music_end()

    def _on_music_end(self):
        """音乐播放结束处理"""
        self.audio._playing = False

        if self.playlist.mode == PlaylistManager.MODE_SINGLE:
            # 单曲循环:重新播放
            song = self.playlist.get_current()
            if song:
                self._play(song)
        elif self.playlist.mode == PlaylistManager.MODE_LOOP:
            # 列表循环:下一曲
            self._next()
        elif self.playlist.mode == PlaylistManager.MODE_RANDOM:
            # 随机:下一曲
            self._next()
        else:
            # 顺序播放:尝试下一曲,没有则停止
            song = self.playlist.next()
            if song:
                self._play(song)
            else:
                self.audio.stop()
                self.btn_play.config(text="&#9654;")

    def run(self):
        menubar = tk.Menu(self.root)
        file_menu = tk.Menu(menubar, tearoff=0)
        file_menu.add_command(label="打开文件...", command=self._open)
        file_menu.add_separator()
        file_menu.add_command(label="保存播放列表", command=self._save_playlist)
        file_menu.add_command(label="加载播放列表", command=self._load_playlist)
        file_menu.add_separator()
        file_menu.add_command(label="退出", command=self._quit_and_save)
        menubar.add_cascade(label="文件", menu=file_menu)

        view_menu = tk.Menu(menubar, tearoff=0)
        view_menu.add_command(label="播放列表", command=self._toggle_list)
        view_menu.add_command(label="歌词", command=self._toggle_lyric)
        view_menu.add_command(label="窗口磁吸", command=self.snap_manager.snap_all_to_main)
        menubar.add_cascade(label="视图", menu=view_menu)

        # 设置菜单
        settings_menu = tk.Menu(menubar, tearoff=0)

        # 透明度子菜单
        opacity_menu = tk.Menu(settings_menu, tearoff=0)
        for pct in [100, 90, 80, 70, 60, 50]:
            opacity_menu.add_command(label=f"{pct}%",
                command=lambda p=pct: self._set_opacity(p))
        settings_menu.add_cascade(label="透明度", menu=opacity_menu)

        settings_menu.add_separator()
        settings_menu.add_command(label="退出并保存状态", command=self._quit_and_save)

        menubar.add_cascade(label="设置", menu=settings_menu)

        self.root.config(menu=menubar)
        self.root.mainloop()

    def _load_config(self) -> dict:
        """加载配置"""
        try:
            if self.CONFIG_FILE.exists():
                with open(self.CONFIG_FILE, 'r', encoding='utf-8') as f:
                    return json.load(f)
        except:
            pass
        return {'opacity': 1.0, 'bg_image': None}

    def _save_config(self):
        """保存配置"""
        try:
            with open(self.CONFIG_FILE, 'w', encoding='utf-8') as f:
                json.dump(self.config, f, ensure_ascii=False, indent=2)
        except Exception as e:
            print(f"保存配置失败: {e}")

    def _apply_background(self):
        """应用背景图片"""
        bg_path = self.config.get('bg_image')
        if bg_path and Path(bg_path).exists():
            try:
                from PIL import Image, ImageTk
                img = Image.open(bg_path)
                # 调整大小以适应窗口
                img = img.resize((480, 420), Image.Resampling.LANCZOS)
                self.bg_image = ImageTk.PhotoImage(img)
                if self.bg_label:
                    self.bg_label.destroy()
                self.bg_label = tk.Label(self.root, image=self.bg_image, bg=self.COLORS['bg_dark'])
                self.bg_label.place(x=0, y=0, relwidth=1, relheight=1)
                self.bg_label.lower()  # 放到最底层
            except ImportError:
                print("PIL未安装,无法使用背景图片")
            except Exception as e:
                print(f"加载背景图片失败: {e}")
        elif self.bg_label:
            self.bg_label.destroy()
            self.bg_label = None

    def _set_background(self):
        """选择背景图片"""
        file = filedialog.askopenfilename(
            title="选择背景图片",
            filetypes=[("图片", "*.jpg *.jpeg *.png *.bmp *.gif")]
        )
        if file:
            self.config['bg_image'] = file
            self._save_config()
            self._apply_background()

    def _clear_background(self):
        """清除背景图片"""
        self.config['bg_image'] = None
        self._save_config()
        self._apply_background()

    def _set_opacity(self, val):
        """设置透明度"""
        opacity = float(val) / 100
        self.config['opacity'] = opacity
        try:
            self.root.attributes('-alpha', opacity)
        except:
            pass
        self._save_config()

    def _save_playlist(self):
        """保存播放列表"""
        try:
            data = {
                'songs': [
                    {'path': s.path, 'title': s.title, 'artist': s.artist,
                     'album': s.album, 'duration': s.duration}
                    for s in self.playlist.songs
                ],
                'current_index': self.playlist.current_index,
                'mode': self.playlist.mode
            }
            with open(self.PLAYLIST_FILE, 'w', encoding='utf-8') as f:
                json.dump(data, f, ensure_ascii=False, indent=2)
            messagebox.showinfo("保存成功", f"播放列表已保存\n共 {len(self.playlist.songs)} 首歌曲")
        except Exception as e:
            messagebox.showerror("保存失败", f"无法保存播放列表: {e}")

    def _load_playlist(self):
        """加载播放列表"""
        try:
            if not self.PLAYLIST_FILE.exists():
                messagebox.showinfo("提示", "没有保存的播放列表")
                return

            with open(self.PLAYLIST_FILE, 'r', encoding='utf-8') as f:
                data = json.load(f)

            # 清空当前列表
            self.playlist.clear()
            self.listbox.delete(0, tk.END)

            # 加载歌曲
            for item in data.get('songs', []):
                if Path(item['path']).exists():
                    song = Song(
                        path=item['path'],
                        title=item.get('title', ''),
                        artist=item.get('artist', ''),
                        album=item.get('album', ''),
                        duration=item.get('duration', 0)
                    )
                    self.playlist.add(song)
                    display = f"{song.title} - {song.artist}" if song.artist else song.title
                    self.listbox.insert(tk.END, display)

            # 恢复播放位置和模式
            self.playlist.current_index = data.get('current_index', -1)
            mode = data.get('mode', PlaylistManager.MODE_ORDER)
            while self.playlist.mode != mode:
                self.playlist.cycle_mode()
            self.btn_mode.config(text=self.MODE_INFO[self.playlist.mode][0])

            messagebox.showinfo("加载成功", f"播放列表已加载\n共 {len(self.playlist.songs)} 首歌曲")
        except Exception as e:
            messagebox.showerror("加载失败", f"无法加载播放列表: {e}")

    def _seek_start(self, event):
        """开始拖拽进度条"""
        self._dragging = True
        self._seek_drag(event)

    def _seek_drag(self, event):
        """拖拽进度条(画两层:背景轨道 + 前景进度 + 指示点)"""
        song = self.playlist.get_current()
        if not song or song.duration <= 0: return

        try:
            w = self.progress_canvas.winfo_reqwidth()
            if w <= 0: w = 460
            x = max(0, min(event.x, w))
            pos = (x / w) * song.duration

            # 实时更新时间显示
            self.time_label.config(text=f"{self._format_time(pos)} / {self._format_time(song.duration)}")

            # --- 画两层:背景轨道(全宽) + 前景进度(当前位置) + 指示点 ---
            self.progress_canvas.delete('all')

            # 第1层:背景轨道(灰色)
            self.progress_canvas.create_rectangle(
                0, 0, w, 15,
                fill=self.COLORS.get('progress_bg', '#3a3a5c'),
                outline=''
            )
            # 第2层:前景进度(彩色)
            self.progress_canvas.create_rectangle(
                0, 0, x, 15,
                fill=self.COLORS['progress_fill'],
                outline=''
            )
            # 第3层:圆点指示器
            self.progress_canvas.create_oval(
                x - 6, 1, x + 6, 14,
                fill=self.COLORS['text'],
                outline=self.COLORS['accent']
            )
        except tk.TclError:
            pass

    def _seek_end(self, event):
        """结束拖拽进度条"""
        song = self.playlist.get_current()
        if song and song.duration > 0:
            # 使用 winfo_reqwidth() 避免 winfo_width() 返回 0 的问题
            w = self.progress_canvas.winfo_reqwidth()
            if w <= 0: w = 460
            x = max(0, min(event.x, w))
            pos = (x / w) * song.duration
            self.audio.set_position(max(0, pos))
            # 拖拽结束后立即刷新进度条显示
            self.root.after_idle(self._update_progress)
        self._dragging = False

    def _format_time(self, seconds: float) -> str:
        """格式化时间显示"""
        mins = int(seconds // 60)
        secs = int(seconds % 60)
        return f"{mins:02d}:{secs:02d}"

    def _quit_and_save(self):
        """退出并保存当前播放状态"""
        try:
            # 保存当前播放状态
            state = {
                'songs': [],
                'current_index': -1,
                'current_position': 0,
                'play_mode': self.playlist.mode,
                'volume': self.volume_scale.get() if hasattr(self, 'volume_scale') else 70,
                'opacity': self.config.get('opacity', 1.0),
                'bg_image': self.config.get('bg_image'),
            }

            # 保存播放列表
            for i, song in enumerate(self.playlist.songs):
                state['songs'].append({
                    'path': song.path,
                    'title': song.title,
                    'artist': song.artist,
                    'album': song.album,
                    'duration': song.duration,
                })
            state['current_index'] = self.playlist.current_index

            # 保存当前播放进度
            if hasattr(self, 'audio') and self.audio.is_playing():
                state['current_position'] = self.audio.get_position()
            # 清除可能残留的旧结束事件(安全调用)
            try:
                pygame.event.clear(pygame.USEREVENT + 1)
            except pygame.error:
                pass  # event subsystem not available

            # 写入状态文件
            with open(self.STATE_FILE, 'w', encoding='utf-8') as f:
                json.dump(state, f, ensure_ascii=False, indent=2)

            # 保存配置
            self._save_config()
        except Exception as e:
            print(f"保存状态失败: {e}")

        # 关闭窗口
        try:
            self.root.destroy()
        except:
            pass

    def _restore_state(self):
        """恢复上次退出时的播放状态"""
        try:
            if not self.STATE_FILE.exists():
                return
            with open(self.STATE_FILE, 'r', encoding='utf-8') as f:
                state = json.load(f)

            songs_data = state.get('songs', [])
            if not songs_data:
                return

            # 恢复播放列表
            valid_songs = []
            for sd in songs_data:
                if Path(sd['path']).exists():
                    song = Song(
                        path=sd['path'],
                        title=sd.get('title', ''),
                        artist=sd.get('artist', ''),
                        album=sd.get('album', ''),
                    )
                    song.duration = sd.get('duration', 0)
                    valid_songs.append(song)
                    self.playlist.add(song)

            if not valid_songs:
                return

            # 恢复播放模式
            mode = state.get('play_mode', PlaylistManager.MODE_CYCLE)
            self.playlist.mode = mode
            if hasattr(self, 'btn_mode'):
                self.btn_mode.config(text=self.MODE_INFO[mode][0])

            # 恢复音量
            vol = state.get('volume', 70)
            if hasattr(self, 'volume_scale'):
                self.volume_scale.set(vol)
                self.audio.set_volume(vol / 100)

            # 恢复播放列表选中
            idx = state.get('current_index', 0)
            if 0 <= idx < len(self.playlist.songs):
                self.list_var.set(self.playlist.songs[idx])
                self.list_box.selection_set(idx)
                self.playlist.current_index = idx

            # 调度自动播放(在窗口稳定后)
            self.root.after(500, self._delayed_restore_play, state)

        except Exception as e:
            print(f"恢复状态失败: {e}")

    def _delayed_restore_play(self, state):
        """延迟自动播放上次曲目"""
        try:
            idx = state.get('current_index', 0)
            if 0 <= idx < len(self.playlist.songs):
                song = self.playlist.songs[idx]
                self._play(song)
                # 恢复播放位置(延迟一点确保音乐已加载)
                pos = state.get('current_position', 0)
                if pos > 0:
                    self.root.after(300, lambda: self.audio.set_position(pos))
        except Exception as e:
            print(f"恢复播放失败: {e}")


if __name__ == "__main__":
    app = DiDiPlayerApp()
    app.run()


737c867097f53f95e5b64ebacfa7848d.png (55.81 KB, 下载次数: 0)

737c867097f53f95e5b64ebacfa7848d.png

80ed5bc377e5aff3790e7e2b402bc6ea.png (220.34 KB, 下载次数: 0)

80ed5bc377e5aff3790e7e2b402bc6ea.png

7e7660183986ce5fc651c6fed4bba91c.png (131.45 KB, 下载次数: 1)

7e7660183986ce5fc651c6fed4bba91c.png

免费评分

参与人数 4吾爱币 +10 热心值 +4 收起 理由
cyzhaojia + 1 + 1 用心讨论,共获提升!
w2rt4 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
a85401234 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
风之暇想 + 7 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

沙发
a85401234 发表于 2026-5-13 13:26
感谢楼主分享,支持原创!!
3#
Myh5200 发表于 2026-5-13 14:12
看着也行啊   啥时候我也可以自己弄一个   努力学习
4#
独孤的云 发表于 2026-5-13 15:09
5#
爱拼才会赢 发表于 2026-5-13 15:32
赛博风格。
6#
w2rt4 发表于 2026-5-13 15:34
谢谢分享
7#
82884019 发表于 2026-5-13 15:37
感谢楼主分享,支持原创!!
不错喜欢的
8#
 楼主| didi2019 发表于 2026-5-13 15:44 |楼主
Myh5200 发表于 2026-5-13 14:12
看着也行啊   啥时候我也可以自己弄一个   努力学习

谢谢夸奖,我就愉快的接受了哈哈哈
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-5-14 04:00

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表