mirror of
https://github.com/hanxi/xiaomusic.git
synced 2025-12-06 14:52:50 +08:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e5df7094c | ||
|
|
64c2f54ff0 | ||
|
|
d1b869ae43 | ||
|
|
d3895f2632 | ||
|
|
5bf62c4b1a | ||
|
|
406e09922c | ||
|
|
ae34572d13 | ||
|
|
1e3c69ea90 |
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
@@ -21,11 +21,6 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: pdm-project/setup-pdm@v3
|
||||
|
||||
- name: Publish package distributions to PyPI
|
||||
run: pdm publish
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
@@ -35,6 +30,11 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
|
||||
- uses: pdm-project/setup-pdm@v3
|
||||
|
||||
- name: Publish package distributions to PyPI
|
||||
run: pdm publish
|
||||
|
||||
build-image:
|
||||
runs-on: ubuntu-latest
|
||||
#needs: release-pypi
|
||||
|
||||
14
CHANGELOG.md
14
CHANGELOG.md
@@ -1,3 +1,17 @@
|
||||
## v0.3.28 (2024-09-03)
|
||||
|
||||
### Feat
|
||||
|
||||
- 新增歌曲收藏功能 #87
|
||||
|
||||
### Fix
|
||||
|
||||
- docker下minetypes无法判断m4a
|
||||
|
||||
### Refactor
|
||||
|
||||
- ffmpeg_location 从配置里读取
|
||||
|
||||
## v0.3.27 (2024-09-02)
|
||||
|
||||
### Feat
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "xiaomusic"
|
||||
version = "0.3.27"
|
||||
version = "0.3.28"
|
||||
description = "Play Music with xiaomi AI speaker"
|
||||
authors = [
|
||||
{name = "涵曦", email = "im.hanxi@gmail.com"},
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.3.27"
|
||||
__version__ = "0.3.28"
|
||||
|
||||
@@ -136,7 +136,7 @@ def main():
|
||||
|
||||
try:
|
||||
filename = config.getsettingfile()
|
||||
with open(filename) as f:
|
||||
with open(filename, encoding="utf-8") as f:
|
||||
data = json.loads(f.read())
|
||||
config.update_config(data)
|
||||
except Exception as e:
|
||||
|
||||
@@ -22,6 +22,8 @@ def default_key_word_dict():
|
||||
"分钟后关机": "stop_after_minute",
|
||||
"播放列表": "play_music_list",
|
||||
"刷新列表": "gen_music_list",
|
||||
"加入收藏": "add_to_favorites",
|
||||
"取消收藏": "del_from_favorites",
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +52,8 @@ def default_key_match_order():
|
||||
"关机",
|
||||
"刷新列表",
|
||||
"播放列表",
|
||||
"加入收藏",
|
||||
"取消收藏",
|
||||
]
|
||||
|
||||
|
||||
@@ -96,6 +100,7 @@ class Config:
|
||||
httpauth_password: str = os.getenv("XIAOMUSIC_HTTPAUTH_PASSWORD", "")
|
||||
music_list_url: str = os.getenv("XIAOMUSIC_MUSIC_LIST_URL", "")
|
||||
music_list_json: str = os.getenv("XIAOMUSIC_MUSIC_LIST_JSON", "")
|
||||
custom_play_list_json: str = os.getenv("XIAOMUSIC_CUSTOM_PLAY_LIST_JSON", "")
|
||||
disable_download: bool = (
|
||||
os.getenv("XIAOMUSIC_DISABLE_DOWNLOAD", "false").lower() == "true"
|
||||
)
|
||||
@@ -153,6 +158,7 @@ class Config:
|
||||
|
||||
def init_keyword(self):
|
||||
self.key_match_order = default_key_match_order()
|
||||
self.key_word_dict = default_key_word_dict()
|
||||
self.append_keyword(self.keywords_playlocal, "playlocal")
|
||||
self.append_keyword(self.keywords_play, "play")
|
||||
self.append_keyword(self.keywords_stop, "stop")
|
||||
|
||||
@@ -12,6 +12,9 @@ $(function(){
|
||||
append_op_button_name("下一首");
|
||||
append_op_button_name("关机");
|
||||
|
||||
append_op_button_name("加入收藏");
|
||||
append_op_button_name("取消收藏");
|
||||
|
||||
$container.append($("<hr>"));
|
||||
|
||||
append_op_button_name("10分钟后关机");
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>Debug For XiaoMusic</title>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1725237467">
|
||||
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1725381307">
|
||||
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
|
||||
<script src="/static/jquery-3.7.1.min.js?version=1725237467"></script>
|
||||
<script src="/static/jquery-3.7.1.min.js?version=1725381307"></script>
|
||||
|
||||
<script>
|
||||
var vConsole = new window.VConsole();
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>小爱音箱操控面板</title>
|
||||
<script src="/static/jquery-3.7.1.min.js?version=1725237467"></script>
|
||||
<script src="/static/app.js?version=1725237467"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1725237467">
|
||||
<script src="/static/jquery-3.7.1.min.js?version=1725381307"></script>
|
||||
<script src="/static/app.js?version=1725381307"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1725381307">
|
||||
|
||||
<!--
|
||||
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>M3U to JSON Converter</title>
|
||||
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1725237467">
|
||||
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1725381307">
|
||||
<!--
|
||||
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
|
||||
<script>
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>小爱音箱操控面板</title>
|
||||
<script src="/static/jquery-3.7.1.min.js?version=1725237467"></script>
|
||||
<script src="/static/setting.js?version=1725237467"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1725237467">
|
||||
<script src="/static/jquery-3.7.1.min.js?version=1725381307"></script>
|
||||
<script src="/static/setting.js?version=1725381307"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/static/style.css?version=1725381307">
|
||||
|
||||
<!--
|
||||
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import copy
|
||||
import difflib
|
||||
import json
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
@@ -189,7 +190,11 @@ def is_mp3(url):
|
||||
return False
|
||||
|
||||
|
||||
async def _get_web_music_duration(session, url, start=0, end=500):
|
||||
def is_m4a(url):
|
||||
return url.endswith(".m4a")
|
||||
|
||||
|
||||
async def _get_web_music_duration(session, url, ffmpeg_location, start=0, end=500):
|
||||
duration = 0
|
||||
headers = {"Range": f"bytes={start}-{end}"}
|
||||
async with session.get(url, headers=headers) as response:
|
||||
@@ -199,6 +204,8 @@ async def _get_web_music_duration(session, url, start=0, end=500):
|
||||
try:
|
||||
if is_mp3(url):
|
||||
m = mutagen.mp3.MP3(tmp)
|
||||
elif is_m4a(url):
|
||||
return get_duration_by_ffprobe(tmp, ffmpeg_location)
|
||||
else:
|
||||
m = mutagen.File(tmp)
|
||||
duration = m.info.length
|
||||
@@ -207,7 +214,7 @@ async def _get_web_music_duration(session, url, start=0, end=500):
|
||||
return duration
|
||||
|
||||
|
||||
async def get_web_music_duration(url):
|
||||
async def get_web_music_duration(url, ffmpeg_location="./ffmpeg/bin"):
|
||||
duration = 0
|
||||
try:
|
||||
parsed_url = urlparse(url)
|
||||
@@ -227,10 +234,12 @@ async def get_web_music_duration(url):
|
||||
# 设置总超时时间为3秒
|
||||
timeout = aiohttp.ClientTimeout(total=3)
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
duration = await _get_web_music_duration(session, url, start=0, end=500)
|
||||
duration = await _get_web_music_duration(
|
||||
session, url, ffmpeg_location, start=0, end=500
|
||||
)
|
||||
if duration <= 0:
|
||||
duration = await _get_web_music_duration(
|
||||
session, url, start=0, end=3000
|
||||
session, url, ffmpeg_location, start=0, end=3000
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(f"Error get_web_music_duration: {e}")
|
||||
@@ -238,12 +247,15 @@ async def get_web_music_duration(url):
|
||||
|
||||
|
||||
# 获取文件播放时长
|
||||
async def get_local_music_duration(filename):
|
||||
async def get_local_music_duration(filename, ffmpeg_location="./ffmpeg/bin"):
|
||||
loop = asyncio.get_event_loop()
|
||||
duration = 0
|
||||
try:
|
||||
if is_mp3(filename):
|
||||
m = await loop.run_in_executor(None, mutagen.mp3.MP3, filename)
|
||||
elif is_m4a(filename):
|
||||
duration = get_duration_by_ffprobe(filename, ffmpeg_location)
|
||||
return duration
|
||||
else:
|
||||
m = await loop.run_in_executor(None, mutagen.File, filename)
|
||||
duration = m.info.length
|
||||
@@ -252,6 +264,33 @@ async def get_local_music_duration(filename):
|
||||
return duration
|
||||
|
||||
|
||||
def get_duration_by_ffprobe(file_path, ffmpeg_location):
|
||||
# 使用 ffprobe 获取文件的元数据,并以 JSON 格式输出
|
||||
result = subprocess.run(
|
||||
[
|
||||
os.path.join(ffmpeg_location, "ffprobe"),
|
||||
"-v",
|
||||
"error", # 只输出错误信息,避免混杂在其他输出中
|
||||
"-show_entries",
|
||||
"format=duration", # 仅显示时长
|
||||
"-of",
|
||||
"json", # 以 JSON 格式输出
|
||||
file_path,
|
||||
],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
)
|
||||
|
||||
# 解析 JSON 输出
|
||||
ffprobe_output = json.loads(result.stdout)
|
||||
|
||||
# 获取时长
|
||||
duration = float(ffprobe_output["format"]["duration"])
|
||||
|
||||
return duration
|
||||
|
||||
|
||||
def get_random(length):
|
||||
return "".join(random.sample(string.ascii_letters + string.digits, length))
|
||||
|
||||
|
||||
@@ -224,7 +224,7 @@ class XiaoMusic:
|
||||
self.log.error(f"{self.mi_token_home} file not exist")
|
||||
return None
|
||||
|
||||
with open(self.mi_token_home) as f:
|
||||
with open(self.mi_token_home, encoding="utf-8") as f:
|
||||
user_data = json.loads(f.read())
|
||||
user_id = user_data.get("userId")
|
||||
service_token = user_data.get("micoapi")[1]
|
||||
@@ -353,13 +353,17 @@ class XiaoMusic:
|
||||
|
||||
if self.is_web_music(name):
|
||||
origin_url = url
|
||||
duration, url = await get_web_music_duration(url)
|
||||
duration, url = await get_web_music_duration(
|
||||
url, self.config.ffmpeg_location
|
||||
)
|
||||
sec = math.ceil(duration)
|
||||
self.log.info(f"网络歌曲 {name} : {origin_url} {url} 的时长 {sec} 秒")
|
||||
else:
|
||||
filename = self.get_filename(name)
|
||||
self.log.info(f"get_music_sec_url. name:{name} filename:{filename}")
|
||||
duration = await get_local_music_duration(filename)
|
||||
duration = await get_local_music_duration(
|
||||
filename, self.config.ffmpeg_location
|
||||
)
|
||||
sec = math.ceil(duration)
|
||||
self.log.info(f"本地歌曲 {name} : {filename} {url} 的时长 {sec} 秒")
|
||||
|
||||
@@ -450,6 +454,8 @@ class XiaoMusic:
|
||||
|
||||
self.music_list["全部"] = list(self.all_music.keys())
|
||||
|
||||
self._append_custom_play_list()
|
||||
|
||||
# 歌单排序
|
||||
for _, play_list in self.music_list.items():
|
||||
play_list.sort(key=custom_sort_key)
|
||||
@@ -458,6 +464,16 @@ class XiaoMusic:
|
||||
for device in self.devices.values():
|
||||
device.update_playlist()
|
||||
|
||||
def _append_custom_play_list(self):
|
||||
if not self.config.custom_play_list_json:
|
||||
return
|
||||
|
||||
try:
|
||||
custom_play_list = json.loads(self.config.custom_play_list_json)
|
||||
self.music_list["收藏"] = list(custom_play_list["收藏"])
|
||||
except Exception as e:
|
||||
self.log.exception(f"Execption {e}")
|
||||
|
||||
# 给歌单里补充网络歌单
|
||||
def _append_music_list(self):
|
||||
if not self.config.music_list_json:
|
||||
@@ -711,6 +727,47 @@ class XiaoMusic:
|
||||
minute = int(arg1)
|
||||
return await self.devices[did].stop_after_minute(minute)
|
||||
|
||||
# 添加歌曲到收藏列表
|
||||
async def add_to_favorites(self, did="", arg1="", **kwargs):
|
||||
name = arg1 if arg1 else self.playingmusic(did)
|
||||
if not name:
|
||||
return
|
||||
|
||||
favorites = self.music_list.get("收藏", [])
|
||||
if name in favorites:
|
||||
return
|
||||
|
||||
favorites.append(name)
|
||||
self.save_favorites(favorites)
|
||||
|
||||
# 从收藏列表中移除
|
||||
async def del_from_favorites(self, did="", arg1="", **kwargs):
|
||||
name = arg1 if arg1 else self.playingmusic(did)
|
||||
if not name:
|
||||
return
|
||||
|
||||
favorites = self.music_list.get("收藏", [])
|
||||
if name not in favorites:
|
||||
return
|
||||
|
||||
favorites.remove(name)
|
||||
self.save_favorites(favorites)
|
||||
|
||||
def save_favorites(self, favorites):
|
||||
self.music_list["收藏"] = favorites
|
||||
custom_play_list = {}
|
||||
if self.config.custom_play_list_json:
|
||||
custom_play_list = json.loads(self.config.custom_play_list_json)
|
||||
custom_play_list["收藏"] = favorites
|
||||
self.config.custom_play_list_json = json.dumps(
|
||||
custom_play_list, ensure_ascii=False
|
||||
)
|
||||
self.save_cur_config()
|
||||
|
||||
# 更新每个设备的歌单
|
||||
for device in self.devices.values():
|
||||
device.update_playlist()
|
||||
|
||||
# 获取音量
|
||||
async def get_volume(self, did="", **kwargs):
|
||||
return await self.devices[did].get_volume()
|
||||
@@ -752,7 +809,7 @@ class XiaoMusic:
|
||||
def try_init_setting(self):
|
||||
try:
|
||||
filename = self.config.getsettingfile()
|
||||
with open(filename) as f:
|
||||
with open(filename, encoding="utf-8") as f:
|
||||
data = json.loads(f.read())
|
||||
self.update_config_from_setting(data)
|
||||
except FileNotFoundError:
|
||||
|
||||
Reference in New Issue
Block a user