1
0
mirror of https://github.com/hanxi/xiaomusic.git synced 2025-12-06 14:52:50 +08:00

Compare commits

..

8 Commits

Author SHA1 Message Date
涵曦
8e5df7094c bump: version 0.3.27 → 0.3.28 2024-09-03 16:35:08 +00:00
涵曦
64c2f54ff0 build: update static version 2024-09-03 16:35:07 +00:00
涵曦
d1b869ae43 feat: 新增歌曲收藏功能 #87 2024-09-03 15:55:19 +00:00
涵曦
d3895f2632 refactor: ffmpeg_location 从配置里读取 2024-09-03 14:33:20 +00:00
hui
5bf62c4b1a fix: docker下minetypes无法判断m4a 2024-09-03 18:33:57 +08:00
hui
406e09922c fix:m4a无法正确获取播放时长 2024-09-03 18:33:57 +08:00
hui
ae34572d13 fix:指定文件编码,修复windows下的文件读取错误 2024-09-03 18:33:57 +08:00
涵曦
1e3c69ea90 Update release.yml 2024-09-02 09:09:49 +08:00
13 changed files with 145 additions and 26 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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"},

View File

@@ -1 +1 @@
__version__ = "0.3.27"
__version__ = "0.3.28"

View File

@@ -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:

View File

@@ -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")

View File

@@ -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分钟后关机");

View File

@@ -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();

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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))

View File

@@ -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: