diff --git a/README.md b/README.md
index 7dc6e09..975e9e4 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,9 @@
> 基于Python的本地Web API项目
+#### v260702
+1. 新增下载接口,同时提供web播放器anian-music.js下载
+
#### v260605
1. 新增期刊查询接口(数据源easyscholar)
diff --git a/data/pan/anian-music.js b/data/pan/anian-music.js
new file mode 100644
index 0000000..d4421cd
--- /dev/null
+++ b/data/pan/anian-music.js
@@ -0,0 +1,460 @@
+(() => {
+ 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);
+}
+
+.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();
+ }
+ });
+
+})();
diff --git a/main.py b/main.py
index ebc31a5..fc6023d 100755
--- a/main.py
+++ b/main.py
@@ -1,4 +1,4 @@
-from flask import Flask, jsonify, request, redirect
+from flask import Flask, jsonify, request, redirect, send_file
from datetime import datetime, date
import ipaddress
from zoneinfo import ZoneInfo, available_timezones
@@ -113,6 +113,7 @@ a:hover{text-decoration:underline;}
| 时间接口 | /time/api | /time/help |
| 谜语接口 | /riddle/api | /riddle/help |
| 期刊接口 | /sci/api | /sci/help |
+| 下载接口 | /pan/api | /pan/help |