commit 3fd0b046b00227d6aff40275217586a1c4647564 Author: anian Date: Fri Jul 17 11:13:35 2026 +0800 新增音乐播放、图片压缩 diff --git a/README.md b/README.md new file mode 100644 index 0000000..5bc4541 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Anian-Tool + +> 一些小工具 + +#### v260717 +1. 新增JS音乐播放器(基于Meting-API) +2. 新增Python图片压缩(基于Pillow) diff --git a/image compression.py b/image compression.py new file mode 100644 index 0000000..85ac9d6 --- /dev/null +++ b/image compression.py @@ -0,0 +1,120 @@ +# pip install pillow +from PIL import Image +import os +import shutil + +def get_file_kb(file_path: str) -> float: + """获取文件大小 KB""" + size_byte = os.path.getsize(file_path) + return size_byte / 1024 + +def compress_to_jpg_loop( + input_path: str, + output_path: str, + min_size_kb: float, + max_size_kb: float, + init_quality: int, + quality_step: int, + min_quality: int, + max_width: int, + max_height: int +): + try: + ori_size = get_file_kb(input_path) + print(f"\n原图: {os.path.basename(input_path)} | 大小: {ori_size:.2f} KB") + + # 小于阈值,直接复制原图,不压缩 + if ori_size < min_size_kb: + shutil.copy2(input_path, output_path) + print(f"✅ 小于{min_size_kb}KB,无需压缩,直接复制") + return + + # 打开图片,处理透明通道 + img = Image.open(input_path) + if img.mode in ("RGBA", "P"): + new_img = Image.new("RGB", img.size, (255, 255, 255)) + mask = img.split()[-1] if img.mode == "RGBA" else None + new_img.paste(img, mask=mask) + img = new_img + + # 等比例缩放 + if max_width and max_height: + img.thumbnail((max_width, max_height)) + + current_quality = init_quality + while True: + img.save(output_path, format="JPEG", quality=current_quality, optimize=True) + out_size = get_file_kb(output_path) + + if out_size <= max_size_kb or current_quality <= min_quality: + print(f"✅ 最终画质 {current_quality} | 输出大小: {out_size:.2f} KB") + break + + current_quality -= quality_step + print(f"⚠️ {out_size:.2f}KB > {max_size_kb}KB,降低画质至 {current_quality} 重新压缩") + + except Exception as e: + print(f"❌ 处理失败 {input_path}:{str(e)}") + + +def batch_compress_folder( + input_dir: str, + output_dir: str, + min_kb, + max_kb, + init_q, + q_step, + min_q, + max_w, + max_h +): + os.makedirs(output_dir, exist_ok=True) + support_ext = (".png", ".jpg", ".jpeg", ".webp", ".bmp") + + for filename in os.listdir(input_dir): + file_path = os.path.join(input_dir, filename) + if not filename.lower().endswith(support_ext): + continue + if os.path.isdir(file_path): + continue + + name_no_ext = os.path.splitext(filename)[0] + out_file = os.path.join(output_dir, f"{name_no_ext}.jpg") + compress_to_jpg_loop( + input_path=file_path, + output_path=out_file, + min_size_kb=min_kb, + max_size_kb=max_kb, + init_quality=init_q, + quality_step=q_step, + min_quality=min_q, + max_width=max_w, + max_height=max_h + ) + + +if __name__ == "__main__": + # ==================== 批量配置区(仅需修改这里) ==================== + INPUT_FOLDER = "./input" + OUTPUT_FOLDER = "./output" + MIN_SIZE_KB = 200 # 小于该KB不压缩 + MAX_SIZE_KB = 800 # 超过该KB循环再压 + INIT_QUALITY = 75 # 初始画质 + QUALITY_STEP = 8 # 每次降低画质数值 + MIN_QUALITY = 20 # 最低画质底线 + LIMIT_W = 2560 # 不需要缩放填 None + LIMIT_H = 1440 + # ================================================================== + + # 仅批量执行,无任何单张代码 + batch_compress_folder( + input_dir=INPUT_FOLDER, + output_dir=OUTPUT_FOLDER, + min_kb=MIN_SIZE_KB, + max_kb=MAX_SIZE_KB, + init_q=INIT_QUALITY, + q_step=QUALITY_STEP, + min_q=MIN_QUALITY, + max_w=LIMIT_W, + max_h=LIMIT_H + ) diff --git a/js-music-player.js b/js-music-player.js new file mode 100644 index 0000000..54c8ee9 --- /dev/null +++ b/js-music-player.js @@ -0,0 +1,467 @@ +(() => { + let songs, audio, currentIndex = 0; + let floatingButton, progressRing, playerPanel, coverImg, popupBgImg, animationFrameId; + let lyricLines = []; + let isPlayerClosed = false; + + const playlistApiUrl = "https://api.i-meto.com/meting/api?server=netease&type=playlist&id=8034031191"; + + // ========================= + // 工具 + // ========================= + const getRandomSongIndex = () => { + if (!songs || !songs.length) return 0; + return Math.floor(Math.random() * songs.length); + }; + + const getCoverSrc = () => { + if (!songs || !songs.length) return "/logo.webp"; + const pic = songs[currentIndex % songs.length]?.pic; + return pic?.trim() ? pic.trim() : "/logo.webp"; + }; + + const setPopupBackground = (src) => { + if (!popupBgImg) return; + + const fallback = "/logo.webp"; + const nextSrc = src?.trim() ? src.trim() : fallback; + popupBgImg.src = nextSrc; + }; + + // ========================= + // 封面 + // ========================= + const setCover = () => { + if (!coverImg) return; + const cover = getCoverSrc(); + coverImg.src = cover; + setPopupBackground(cover); + }; + + // ========================= + // audio + // ========================= + const initAudio = () => { + if (!audio) { + audio = new Audio(); + audio.volume = 0.5; + + audio.onplay = () => { syncUI(); setRotate(); }; + audio.onpause = () => { syncUI(); setRotate(); }; + audio.onended = () => next(); + } + }; + + // ========================= + // LRC解析 + // ========================= + const parseLrc = (text) => { + const lines = text.split("\n"); + const res = []; + const reg = /\[(\d{2}):(\d{2})\.(\d{2,3})\]/; + + for (let line of lines) { + const timeMatch = line.match(reg); + if (!timeMatch) continue; + + const time = + (+timeMatch[1]) * 60 + + (+timeMatch[2]) + + (+timeMatch[3]) / (timeMatch[3].length === 3 ? 1000 : 100); + + const txt = line.replace(reg, "").trim(); + if (txt) res.push({ time, text: txt }); + } + + return res.sort((left, right) => left.time - right.time); + }; + + // ========================= + // 歌词渲染 + // ========================= + const renderLrc = (top, mid, bottom) => { + const box = playerPanel?.querySelector(".lrc"); + if (!box) return; + + box.innerHTML = ` +
${top}
+
${mid}
+
${bottom}
+ `; + }; + + const renderLoadingLrc = () => { + renderLrc("加", "载", "中"); + }; + + // ========================= + // loading歌词 + // ========================= + const loadLyric = (song) => { + lyricLines = []; + + renderLoadingLrc(); + + if (!song?.lrc) { + renderLrc("-", "暂无歌词", "-"); + return; + } + + fetch(song.lrc) + .then(response => response.text()) + .then(text => { + lyricLines = parseLrc(text); + }) + .catch(() => { + lyricLines = []; + renderLrc("-", "加载失败", "-"); + }); + }; + + // ========================= + // load song + // ========================= + const loadSong = () => { + if (!songs || !songs.length) return; + + const currentSong = songs[currentIndex % songs.length]; + initAudio(); + + audio.src = currentSong.url; + loadLyric(currentSong); + }; + + // ========================= + // UI(标题) + // ========================= + const syncUI = () => { + if (!playerPanel) return; + + const titleBox = playerPanel.querySelector(".t"); + if (!titleBox) return; + + const currentSong = songs?.length ? songs[currentIndex % songs.length] : null; + + titleBox.innerHTML = ` +
+
+
${currentSong?.title || "加载中"}
+
${currentSong?.author || "请稍候"}
+
+ +
+ + + +
+
+ `; + + const isPlaying = audio && !audio.paused; + playerPanel.querySelector(".pp").textContent = isPlaying ? "暂停" : "播放"; + + // 关闭(彻底关闭) + titleBox.querySelector(".closePlayerBtn").onclick = (event) => { + event.stopPropagation(); + + if (audio) { + audio.pause(); + audio.src = ""; + } + + isPlayerClosed = true; + + floatingButton.style.display = "none"; + playerPanel.style.display = "none"; + }; + + // 最小化(只收起面板) + titleBox.querySelector(".minPlayerBtn").onclick = (event) => { + event.stopPropagation(); + if (playerPanel) playerPanel.style.display = "none"; + }; + }; + + // ========================= + // rotate + // ========================= + const setRotate = () => { + if (!coverImg) return; + coverImg.style.animationPlayState = (audio && !audio.paused) ? "running" : "paused"; + }; + + // ========================= + // 歌词更新 + // ========================= + const updateLrc = () => { + if (!lyricLines.length || !audio) return; + + const currentTime = audio.currentTime; + + let lyricIndex = 0; + for (let lyricLineIndex = 0; lyricLineIndex < lyricLines.length; lyricLineIndex++) { + if (currentTime >= lyricLines[lyricLineIndex].time) lyricIndex = lyricLineIndex; + else break; + } + + renderLrc( + lyricLines[lyricIndex - 1]?.text || "-", + lyricLines[lyricIndex]?.text || "-", + lyricLines[lyricIndex + 1]?.text || "-" + ); + }; + + // ========================= + // progress + // ========================= + const progress = () => { + if (!audio || !audio.duration) return; + + const progressPercent = audio.currentTime / audio.duration; + + progressRing.style.background = + `conic-gradient(#fff ${progressPercent * 360}deg,rgba(255,255,255,.15) 0)`; + + const seek = playerPanel?.querySelector(".seek"); + if (seek && !seek.matches(":active")) { + seek.value = Math.floor(progressPercent * 1000); + } + + updateLrc(); + }; + + // ========================= + // play / pause / next + // ========================= + const play = async () => { + initAudio(); + if (!audio.src) loadSong(); + + try { await audio.play(); } catch { } + + syncUI(); + setRotate(); + + cancelAnimationFrame(animationFrameId); + loop(); + }; + + const pause = () => { + if (audio) audio.pause(); + syncUI(); + setRotate(); + }; + + const next = () => { + if (!songs || !songs.length) return; + + let newIndex = getRandomSongIndex(); + if (songs.length > 1) { + while (newIndex === currentIndex) newIndex = getRandomSongIndex(); + } + + currentIndex = newIndex; + + const currentSong = songs[currentIndex % songs.length]; + + loadSong(); + syncUI(); + setRotate(); + setCover(); + + loadLyric(currentSong); + + audio.play().catch(() => { }); + }; + + // ========================= + // volume + // ========================= + const volUp = () => { + if (!audio) return; + audio.volume = Math.min(1, audio.volume + 0.1); + }; + + const volDown = () => { + if (!audio) return; + audio.volume = Math.max(0, audio.volume - 0.1); + }; + + // ========================= + // loop + // ========================= + const loop = () => { + progress(); + animationFrameId = requestAnimationFrame(loop); + }; + + // ========================= + // UI init + // ========================= + const initUI = () => { + floatingButton = document.createElement("div"); + floatingButton.style.cssText = + "position:fixed;right:18px;bottom:18px;width:58px;height:58px;border-radius:50%;background:#111;z-index:99999;display:flex;align-items:center;justify-content:center;cursor:pointer;overflow:hidden"; + + progressRing = document.createElement("div"); + progressRing.style.cssText = + "position:absolute;inset:0;border-radius:50%;background:conic-gradient(rgba(255,255,255,.15) 0deg,transparent 0);z-index:0"; + + coverImg = document.createElement("img"); + coverImg.src = "/logo.webp"; + coverImg.style.cssText = + "width:46px;height:46px;border-radius:50%;object-fit:cover;z-index:2;position:relative;animation:spin 6s linear infinite;animation-play-state:paused"; + + floatingButton.appendChild(progressRing); + floatingButton.appendChild(coverImg); + + playerPanel = document.createElement("div"); + playerPanel.style.cssText = + "position:fixed;right:18px;bottom:90px;width:240px;background:#000;color:#fff;border-radius:12px;display:none;z-index:99999;overflow:hidden"; + + playerPanel.innerHTML = ` +
+
+ +
+ + + + +
`; + + const popupBg = document.createElement("img"); + popupBg.className = "popupBg"; + popupBg.alt = ""; + + const popupShade = document.createElement("div"); + popupShade.className = "popupShade"; + + const popupContent = document.createElement("div"); + popupContent.className = "popupContent"; + + while (playerPanel.firstChild) popupContent.appendChild(playerPanel.firstChild); + + playerPanel.appendChild(popupBg); + playerPanel.appendChild(popupShade); + playerPanel.appendChild(popupContent); + + popupBgImg = popupBg; + setPopupBackground("/logo.webp"); + + document.body.appendChild(floatingButton); + document.body.appendChild(playerPanel); + + const style = document.createElement("style"); + style.textContent = ` +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +button { + transition: transform .05s ease, background .05s ease; +} + +button:active { + transform: scale(0.92); +} + +.pp, .cc, .vu, .vd { + background:transparent; + color:#fff; + border:none; + font-size:16px; +} + +.popupBg { + position:absolute; + inset:0; + width:100%; + height:100%; + object-fit:cover; + z-index:0; +} + +.popupShade { + position:absolute; + inset:0; + background:rgba(0,0,0,.58); + z-index:1; +} + +.popupContent { + position:relative; + z-index:2; + padding:10px; +} +`; + document.head.appendChild(style); + + floatingButton.onclick = () => { + if (isPlayerClosed) return; + playerPanel.style.display = playerPanel.style.display === "none" ? "block" : "none"; + }; + + playerPanel.querySelector(".pp").onclick = (event) => { + event.stopPropagation(); + if (!audio || audio.paused) play(); + else pause(); + }; + + playerPanel.querySelector(".cc").onclick = () => next(); + playerPanel.querySelector(".vu").onclick = () => volUp(); + playerPanel.querySelector(".vd").onclick = () => volDown(); + + const seek = playerPanel.querySelector(".seek"); + seek.addEventListener("input", () => { + if (!audio || !audio.duration) return; + audio.currentTime = (seek.value / 1000) * audio.duration; + }); + + setInterval(progress, 100); + }; + + // ========================= + // init + // ========================= + initUI(); + + renderLoadingLrc(); + syncUI(); + + fetch(playlistApiUrl) + .then(response => response.json()) + .then(responseSongs => { + songs = responseSongs; + + if (songs && songs.length) { + currentIndex = getRandomSongIndex(); + + setCover(); + syncUI(); + + loadSong(); + } else { + syncUI(); + } + }); + +})();