mirror of
https://github.com/hanxi/xiaomusic.git
synced 2025-12-06 14:52:50 +08:00
Compare commits
214 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d758a47f4 | ||
|
|
a1a025960b | ||
|
|
2c5ffa5645 | ||
|
|
f78df85bb9 | ||
|
|
a771441616 | ||
|
|
0ef36d26ff | ||
|
|
589d36f98c | ||
|
|
ad9ce10fa2 | ||
|
|
bea986f4ef | ||
|
|
c3451fea6a | ||
|
|
6ce99a0e62 | ||
|
|
063688f3e6 | ||
|
|
0c4b34a554 | ||
|
|
52b6000342 | ||
|
|
a1962cd450 | ||
|
|
2520e5d9e5 | ||
|
|
bd0a3a1052 | ||
|
|
24218babeb | ||
|
|
dd803785e1 | ||
|
|
78ee51c8c2 | ||
|
|
4e3e127767 | ||
|
|
dafd2cec05 | ||
|
|
59e9191f70 | ||
|
|
f7286269af | ||
|
|
d98652fe5d | ||
|
|
497dd07030 | ||
|
|
6cbec5bbff | ||
|
|
c46c652e5d | ||
|
|
1fbbae5f1c | ||
|
|
ffd04fab68 | ||
|
|
51dfe70a87 | ||
|
|
956c569b93 | ||
|
|
617a961816 | ||
|
|
b37b6fd57c | ||
|
|
c09baaac15 | ||
|
|
7ded7a08dd | ||
|
|
f4487945db | ||
|
|
0bdd2986f1 | ||
|
|
a72777317e | ||
|
|
f2e096da38 | ||
|
|
f42398ec9f | ||
|
|
5121d141b4 | ||
|
|
7cf9751dde | ||
|
|
990defefc9 | ||
|
|
4e6afa0e3e | ||
|
|
e501097ec2 | ||
|
|
ea567fd55a | ||
|
|
9cb1931f90 | ||
|
|
e50db9ea59 | ||
|
|
5c788ccaed | ||
|
|
10a63e5568 | ||
|
|
2460cd3207 | ||
|
|
2595451280 | ||
|
|
2c4b131c19 | ||
|
|
fb84f36b55 | ||
|
|
811e9377f7 | ||
|
|
00733ad669 | ||
|
|
4cdfd68c4d | ||
|
|
2b28bf8551 | ||
|
|
1023caf80a | ||
|
|
fba021d8fb | ||
|
|
8bac3ee961 | ||
|
|
2f18e1935e | ||
|
|
6c208eb1ce | ||
|
|
07c679c304 | ||
|
|
f7551d6b3e | ||
|
|
25044a8157 | ||
|
|
34d33fee29 | ||
|
|
772a9c4e53 | ||
|
|
af06281ac3 | ||
|
|
42a6d0fe77 | ||
|
|
7df7ed7110 | ||
|
|
1b20617588 | ||
|
|
94921eb12a | ||
|
|
ce4a4149d4 | ||
|
|
108918ca8a | ||
|
|
c25773e087 | ||
|
|
df6b71b47a | ||
|
|
b0dfb37d98 | ||
|
|
a510f9162e | ||
|
|
af1d0dc285 | ||
|
|
7301953592 | ||
|
|
0238982925 | ||
|
|
3e9f2aac1b | ||
|
|
b32aca16bc | ||
|
|
9bae1397ba | ||
|
|
6693272da5 | ||
|
|
136d0efde4 | ||
|
|
fa67b6deb1 | ||
|
|
998e13c2fe | ||
|
|
93ea4c9736 | ||
|
|
42eea14335 | ||
|
|
558f00a5a4 | ||
|
|
78bf7d30f5 | ||
|
|
22f8711818 | ||
|
|
740b24131f | ||
|
|
9079a7d79a | ||
|
|
c5ec98a527 | ||
|
|
569ae234ca | ||
|
|
8c021e5889 | ||
|
|
d30cb67bc1 | ||
|
|
8e0fc0bf03 | ||
|
|
3dfc6b4bb4 | ||
|
|
87da35ca4c | ||
|
|
e3c0586b10 | ||
|
|
b903a14304 | ||
|
|
3691dbbcf3 | ||
|
|
d8e68f7492 | ||
|
|
b617f08032 | ||
|
|
f46d2f60dc | ||
|
|
acad787c9f | ||
|
|
bf2a25e7c5 | ||
|
|
6987ff35f7 | ||
|
|
c256857fe6 | ||
|
|
c5708fc5f5 | ||
|
|
ea665b0f81 | ||
|
|
dd6d2efddd | ||
|
|
f0e344649e | ||
|
|
21296967bf | ||
|
|
71af5f8d0e | ||
|
|
14e7a151a3 | ||
|
|
24a40882df | ||
|
|
1d8732d3da | ||
|
|
499728df0b | ||
|
|
62cf39a959 | ||
|
|
7c4af5d548 | ||
|
|
93c20f2f94 | ||
|
|
ffb79f0fc3 | ||
|
|
161c0d738b | ||
|
|
52a5574220 | ||
|
|
8e0f7a4b46 | ||
|
|
f060665027 | ||
|
|
dfc79e1b5f | ||
|
|
3737c7359f | ||
|
|
96083bee89 | ||
|
|
92ed00f7cc | ||
|
|
37a82e8c98 | ||
|
|
361d25786b | ||
|
|
3fe5215f59 | ||
|
|
5163f22f7d | ||
|
|
971b8efb3f | ||
|
|
681b65815a | ||
|
|
1015736635 | ||
|
|
1e0949703f | ||
|
|
8dc18ad208 | ||
|
|
74d772e242 | ||
|
|
2815f058ec | ||
|
|
31eeb0a1cf | ||
|
|
3468e4a979 | ||
|
|
9847f7afc2 | ||
|
|
d7ee021dc9 | ||
|
|
fd6d1ca66a | ||
|
|
23882cf85c | ||
|
|
73df9dec48 | ||
|
|
accc449824 | ||
|
|
59e096b2ce | ||
|
|
12532b48d6 | ||
|
|
9ed39d0c02 | ||
|
|
c7045e4071 | ||
|
|
2a443915d5 | ||
|
|
5fbaf89d1b | ||
|
|
c1994b5bc6 | ||
|
|
4737a4c2e6 | ||
|
|
40c0b740ae | ||
|
|
0ba125c63c | ||
|
|
f44ec11a53 | ||
|
|
baedee5638 | ||
|
|
e434faf684 | ||
|
|
8b746b32dc | ||
|
|
977ff684eb | ||
|
|
c552a34ec1 | ||
|
|
e4144aa568 | ||
|
|
fe54ad29e2 | ||
|
|
48a8bf3325 | ||
|
|
2c5b7b7f26 | ||
|
|
46168315d0 | ||
|
|
b385bfab54 | ||
|
|
4e2e3e1f74 | ||
|
|
0db3e4cada | ||
|
|
fa21d02e1d | ||
|
|
be48fe9f54 | ||
|
|
042a91336e | ||
|
|
790a4cd808 | ||
|
|
70d029f4d1 | ||
|
|
0472ad3188 | ||
|
|
733c44d12f | ||
|
|
8c92afd09b | ||
|
|
742929b9d5 | ||
|
|
4766e08ff4 | ||
|
|
99c4296c7a | ||
|
|
0ae24f4810 | ||
|
|
77cca0d18c | ||
|
|
ddc24595df | ||
|
|
ce1bfb164c | ||
|
|
78dbba6c1f | ||
|
|
7eb0ab1e46 | ||
|
|
0a2e3cc579 | ||
|
|
75ec336285 | ||
|
|
a19d53f000 | ||
|
|
cdb6190e7a | ||
|
|
1aa9b58561 | ||
|
|
6a990f48d0 | ||
|
|
82e92a0380 | ||
|
|
ecce5c8848 | ||
|
|
87fb34e5c9 | ||
|
|
df3c4b7fa9 | ||
|
|
cb2af0ee9f | ||
|
|
8f9bba0ca3 | ||
|
|
b86e8d3196 | ||
|
|
65067346f3 | ||
|
|
865b412fb7 | ||
|
|
441fffd59e | ||
|
|
2ee7b956cf | ||
|
|
126bfa43a2 |
1
.github/workflows/build-base-image.yml
vendored
1
.github/workflows/build-base-image.yml
vendored
@@ -2,6 +2,7 @@ name: Build Docker Base Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths:
|
||||
- 'Dockerfile.builder'
|
||||
- 'Dockerfile.runtime'
|
||||
|
||||
208
.github/workflows/ci.yml
vendored
208
.github/workflows/ci.yml
vendored
@@ -1,18 +1,31 @@
|
||||
name: ci
|
||||
name: CI Workflow
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "*"
|
||||
- "*" # 所有分支触发
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
TEST_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:${{ github.ref_name }}
|
||||
LATEST_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:latest
|
||||
STABLE_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:stable
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
build-test-publish:
|
||||
# Job 构建镜像
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
@@ -26,6 +39,7 @@ jobs:
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build Docker image (linux/amd64)
|
||||
id: build_amd64
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/amd64
|
||||
@@ -37,6 +51,7 @@ jobs:
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache-new
|
||||
|
||||
- name: Build Docker image (linux/arm64)
|
||||
id: build_arm64
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/arm64
|
||||
@@ -48,6 +63,7 @@ jobs:
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache-new
|
||||
|
||||
- name: Build Docker image (linux/arm/v7)
|
||||
id: build_armv7
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/arm/v7
|
||||
@@ -58,14 +74,89 @@ jobs:
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache-new
|
||||
|
||||
# We test all the images on amd64 host here. This uses QEMU to emulate
|
||||
# the other platforms.
|
||||
- run: docker run --rm ${TEST_TAG}-linux-amd64 -h
|
||||
- run: docker run --rm ${TEST_TAG}-linux-arm64 -h
|
||||
- run: docker run --rm ${TEST_TAG}-linux-arm-v7 -h
|
||||
- name: List Docker images
|
||||
run: docker images
|
||||
|
||||
# This will only push the previously built images.
|
||||
- name: Publish to Docker Hub
|
||||
- name: Test amd64 image
|
||||
run: |
|
||||
docker run --rm ${{ env.TEST_TAG }}-linux-amd64 /app/.venv/bin/python3 /app/xiaomusic.py -h
|
||||
|
||||
- name: Test arm64 image
|
||||
run: |
|
||||
docker run --rm ${{ env.TEST_TAG }}-linux-arm64 /app/.venv/bin/python3 /app/xiaomusic.py -h
|
||||
|
||||
- name: Test armv7 image
|
||||
run: |
|
||||
docker run --rm ${{ env.TEST_TAG }}-linux-arm-v7 /app/.venv/bin/python3 /app/xiaomusic.py -h
|
||||
|
||||
- name: Docker Hub Description
|
||||
if: github.ref == 'refs/heads/main'
|
||||
uses: peter-evans/dockerhub-description@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
repository: hanxi/xiaomusic
|
||||
|
||||
# 发布 PyPI 版本
|
||||
# 仅在 ref 为以 v 开头的标签时执行
|
||||
- uses: actions/setup-node@v4
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
registry-url: https://registry.npmjs.org/
|
||||
node-version: lts/*
|
||||
|
||||
- run: npx changelogithub
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
continue-on-error: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
|
||||
- uses: pdm-project/setup-pdm@v3
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
|
||||
- name: Publish package distributions to PyPI
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
run: pdm publish
|
||||
continue-on-error: true
|
||||
|
||||
|
||||
# Job 打包应用, 发布镜像和 Release
|
||||
# 仅在 main 分支或以 v 开头的标签运行
|
||||
- name: Package /app for amd64
|
||||
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
|
||||
run: |
|
||||
docker run --rm -v $PWD:/workspace ${{ env.TEST_TAG }}-linux-amd64 tar czf /workspace/app-amd64.tar.gz -C / app
|
||||
|
||||
- name: Package /app (lite) for amd64
|
||||
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
|
||||
run: |
|
||||
docker run --rm -v $PWD:/workspace ${{ env.TEST_TAG }}-linux-amd64 bash -c \
|
||||
"cd /app && tar --exclude='ffmpeg' -czf /workspace/app-amd64-lite.tar.gz .[!.]* *"
|
||||
|
||||
- name: Package /app for arm64
|
||||
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
|
||||
run: |
|
||||
docker run --rm -v $PWD:/workspace ${{ env.TEST_TAG }}-linux-arm64 tar czf /workspace/app-arm64.tar.gz -C / app
|
||||
|
||||
- name: Package /app (lite) for arm64
|
||||
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
|
||||
run: |
|
||||
docker run --rm -v $PWD:/workspace ${{ env.TEST_TAG }}-linux-arm64 bash -c \
|
||||
"cd /app && tar --exclude='ffmpeg' -czf /workspace/app-arm64-lite.tar.gz .[!.]* *"
|
||||
|
||||
- name: Package /app for arm/v7
|
||||
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
|
||||
run: |
|
||||
docker run --rm -v $PWD:/workspace ${{ env.TEST_TAG }}-linux-arm-v7 tar czf /workspace/app-arm-v7.tar.gz -C / app
|
||||
|
||||
- name: Package /app (lite) for arm/v7
|
||||
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
|
||||
run: |
|
||||
docker run --rm -v $PWD:/workspace ${{ env.TEST_TAG }}-linux-arm-v7 bash -c \
|
||||
"cd /app && tar --exclude='ffmpeg' -czf /workspace/app-arm-v7-lite.tar.gz .[!.]* *"
|
||||
|
||||
- name: Publish to Docker Hub main
|
||||
if: github.ref == 'refs/heads/main'
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
@@ -75,14 +166,97 @@ jobs:
|
||||
cache-from: type=local,src=/tmp/.buildx-cache-new
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache-new
|
||||
|
||||
- name: Docker Hub Description
|
||||
uses: peter-evans/dockerhub-description@v4
|
||||
# 仅在 ref 为以 v 开头的标签时执行
|
||||
- name: Publish to Docker Hub latest and stable
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
repository: hanxi/xiaomusic
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ env.TEST_TAG }}, ${{ env.LATEST_TAG }}, ${{ env.STABLE_TAG }}
|
||||
cache-from: type=local,src=/tmp/.buildx-cache-new
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache-new
|
||||
|
||||
- name: Move cache to limit growth
|
||||
run: |
|
||||
rm -rf /tmp/.buildx-cache
|
||||
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
||||
|
||||
# 上传文件到 release 页面
|
||||
- name: Install GitHub CLI
|
||||
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y gh
|
||||
|
||||
# 创建或更新 Release
|
||||
- name: Create or update Release main
|
||||
if: github.ref == 'refs/heads/main'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # 设置 GH_TOKEN 环境变量
|
||||
run: |
|
||||
RELEASE_NAME=test
|
||||
RELEASE_BODY="This release is automatically updated from the main branch."
|
||||
|
||||
# 检查是否已有同名 Release
|
||||
EXISTING_RELEASE=$(gh release view "${RELEASE_NAME}" --json id --jq .id || echo "")
|
||||
|
||||
if [[ -n "${EXISTING_RELEASE}" ]]; then
|
||||
echo "release exist"
|
||||
else
|
||||
# 创建新的 Release
|
||||
gh release create "${RELEASE_NAME}" \
|
||||
--prerelease=false \
|
||||
--title "${RELEASE_NAME}" \
|
||||
--notes "${RELEASE_BODY}"
|
||||
fi
|
||||
|
||||
- name: Create or update Release tag
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # 设置 GH_TOKEN 环境变量
|
||||
run: |
|
||||
RELEASE_NAME=${{ github.ref_name }}
|
||||
RELEASE_BODY="This release is automatically updated from the ${RELEASE_NAME} branch."
|
||||
|
||||
# 检查是否已有同名 Release
|
||||
EXISTING_RELEASE=$(gh release view "${RELEASE_NAME}" --json id --jq .id || echo "")
|
||||
|
||||
if [[ -n "${EXISTING_RELEASE}" ]]; then
|
||||
echo "release exist"
|
||||
else
|
||||
# 创建新的 Release
|
||||
gh release create "${RELEASE_NAME}" \
|
||||
--prerelease=false \
|
||||
--title "${RELEASE_NAME}" \
|
||||
--notes "${RELEASE_BODY}"
|
||||
fi
|
||||
|
||||
|
||||
# 上传多个文件并覆盖
|
||||
- name: Upload assets to Release main
|
||||
if: github.ref == 'refs/heads/main'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # 设置 GH_TOKEN 环境变量
|
||||
run: |
|
||||
RELEASE_NAME=test
|
||||
|
||||
FILES=$(find . -type f -name "app-*.tar.gz")
|
||||
for FILE in ${FILES}; do
|
||||
echo "type upload ${FILE}"
|
||||
gh release upload "${RELEASE_NAME}" "${FILE}" --clobber
|
||||
done
|
||||
|
||||
- name: Upload assets to Release tag
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # 设置 GH_TOKEN 环境变量
|
||||
run: |
|
||||
RELEASE_NAME=${{ github.ref_name }}
|
||||
|
||||
FILES=$(find . -type f -name "app-*.tar.gz")
|
||||
for FILE in ${FILES}; do
|
||||
echo "type upload ${FILE}"
|
||||
gh release upload "${RELEASE_NAME}" "${FILE}" --clobber
|
||||
done
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
- name: install ruff
|
||||
run: pip install ruff
|
||||
- name: Format code
|
||||
run: pdm fmt && pdm lint --fix
|
||||
run: pdm lintfmt
|
||||
|
||||
- name: Check for changes
|
||||
id: check_changes
|
||||
60
.github/workflows/release.yml
vendored
60
.github/workflows/release.yml
vendored
@@ -1,60 +0,0 @@
|
||||
name: Release and Build Docker Image
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
pypi-publish:
|
||||
name: upload release to PyPI
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
registry-url: https://registry.npmjs.org/
|
||||
node-version: lts/*
|
||||
|
||||
- run: npx changelogithub
|
||||
continue-on-error: true
|
||||
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
|
||||
# run unless event type is pull_request
|
||||
if: github.event_name != 'pull_request'
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64, linux/arm64, linux/arm/v7
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:${{ github.ref_name }}, ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:latest, ${{ secrets.DOCKERHUB_USERNAME }}/xiaomusic:stable
|
||||
104
.github/workflows/static.yml
vendored
Normal file
104
.github/workflows/static.yml
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
# Simple workflow for deploying static content to GitHub Pages
|
||||
name: Deploy static content to Pages
|
||||
|
||||
on:
|
||||
# Runs on pushes targeting the default branch
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- 'README.md'
|
||||
- 'CHANGELOG.md'
|
||||
- '.github/workflows/static.yml'
|
||||
# Runs on issue events
|
||||
issues:
|
||||
types: [opened, edited, reopened] # Specify events you're interested in
|
||||
|
||||
release:
|
||||
types:
|
||||
- uploaded
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: write
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
||||
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# Single deploy job since we're just deploying
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '20'
|
||||
- name: Install dependencies
|
||||
working-directory: ./docs # 指定工作目录为 docs
|
||||
run: |
|
||||
npm install
|
||||
|
||||
- name: Build VitePress
|
||||
env:
|
||||
VITE_GITHUB_ISSUES_TOKEN: ${{ secrets.VITE_GITHUB_ISSUES_TOKEN }}
|
||||
working-directory: ./docs # 指定工作目录为 docs
|
||||
run: | # 有点小问题,得执行2次
|
||||
npm run docs:build
|
||||
npm run docs:build
|
||||
|
||||
- uses: pdm-project/setup-pdm@v3
|
||||
- name: Install dependencies
|
||||
run: pdm install
|
||||
- name: generate versions.json
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: pdm run get_release.py
|
||||
|
||||
- name: Check for changes
|
||||
id: check_changes
|
||||
run: |
|
||||
if [ -n "$(git diff docs)" ]; then
|
||||
echo "changed=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "changed=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
continue-on-error: true
|
||||
|
||||
# Optionally, customize the user name and commit message, and can add an email as well such as Github Actions' email
|
||||
- name: Set up Git and Commit Changes
|
||||
run: |
|
||||
if [ "${{ steps.check_changes.outputs.changed }}" == "true" ]; then
|
||||
git config --local user.name "Issues Docs [BOT]"
|
||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add .
|
||||
git commit -m "Auto-Generate docs 🤖"
|
||||
git push
|
||||
fi
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
# Upload entire repository
|
||||
path: './docs/.vitepress/dist'
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -169,3 +169,5 @@ setting.json
|
||||
.DS_Store
|
||||
cache
|
||||
tmp/
|
||||
xiaomusic.log.txt
|
||||
node_modules
|
||||
|
||||
181
CHANGELOG.md
181
CHANGELOG.md
@@ -1,3 +1,184 @@
|
||||
## v0.3.67 (2024-12-29)
|
||||
|
||||
### Feat
|
||||
|
||||
- 简化设置,不允许修改监听端口
|
||||
|
||||
### Fix
|
||||
|
||||
- 修复默认主题搜索问题
|
||||
|
||||
## v0.3.66 (2024-12-26)
|
||||
|
||||
### Fix
|
||||
|
||||
- 修复歌曲批量重命名的问题
|
||||
- 修复自定义歌单删除后没刷新歌单列表
|
||||
- 尝试修复更新失败问题
|
||||
|
||||
## v0.3.65 (2024-12-24)
|
||||
|
||||
### Fix
|
||||
|
||||
- 处理图像报错
|
||||
- 修改歌单名字漏更新歌单列表
|
||||
- 修复获取自定义歌单接口报错
|
||||
|
||||
## v0.3.64 (2024-12-22)
|
||||
|
||||
### Fix
|
||||
|
||||
- 使用自己架设的 sentry 服务,解决 Cloudflare 额度超量问题
|
||||
|
||||
## v0.3.63 (2024-12-22)
|
||||
|
||||
### Perf
|
||||
|
||||
- 只监控报错信息
|
||||
|
||||
## v0.3.62 (2024-12-21)
|
||||
|
||||
### Fix
|
||||
|
||||
- 修复首次配置时,默认主题只有一个设备的问题。
|
||||
- 修复一些报错问题
|
||||
|
||||
## v0.3.61 (2024-12-19)
|
||||
|
||||
### Fix
|
||||
|
||||
- 尝试修复更新问题
|
||||
|
||||
### Refactor
|
||||
|
||||
- 重构更新流程
|
||||
|
||||
## v0.3.60 (2024-12-19)
|
||||
|
||||
## v0.3.59 (2024-12-19)
|
||||
|
||||
### Feat
|
||||
|
||||
- 新增更多的歌单编辑相关接口
|
||||
- 一键更新功能
|
||||
- 接入 sentry 平台
|
||||
- 实现更新接口
|
||||
- 下载完成之后修改文件权限为755 close #316
|
||||
|
||||
### Fix
|
||||
|
||||
- 解决飞牛平台报错问题
|
||||
|
||||
### Refactor
|
||||
|
||||
- 优化代码日志级别
|
||||
- 更新等消息推送到客户端再重启
|
||||
- 更新接口修改
|
||||
|
||||
## v0.3.58 (2024-12-15)
|
||||
|
||||
### Fix
|
||||
|
||||
- 尝试解决七牛设备问题
|
||||
- 修复 umami 统计函数报错,解决七牛环境问题
|
||||
|
||||
## v0.3.57 (2024-12-14)
|
||||
|
||||
### Feat
|
||||
|
||||
- 优化批量下载工具命名和下载高码率音频
|
||||
- 新增搜索播放口令用于生成临时播放列表
|
||||
- 新增设置项 ignore_tag_dirs 用于忽略读取目录下的标签信息,解决挂载 alist 目录的问题
|
||||
- 新主题 Material (#299)
|
||||
|
||||
### Fix
|
||||
|
||||
- 默认主题刷新时选中当前播放歌曲
|
||||
- 修复当前播放列表没更新的问题
|
||||
- 修复搜索时不显示保存输入框问题
|
||||
- 收藏 (#301)
|
||||
- 修复收藏歌曲失败的问题
|
||||
- 小屏幕设备主页显示问题 (#300)
|
||||
|
||||
### Refactor
|
||||
|
||||
- 修改默认主题
|
||||
- 后端也加入 umami 统计
|
||||
- 新增自托管 umami 统计
|
||||
- XIAOMUSIC_HOSTNAME 携带端口号友好提醒并处理 (#303)
|
||||
- 修改标题
|
||||
- 冲突解决错误
|
||||
|
||||
## v0.3.56 (2024-12-07)
|
||||
|
||||
### Feat
|
||||
|
||||
- tag 信息支持写入到歌曲文件 see #266
|
||||
- 开启gzip压缩
|
||||
|
||||
### Fix
|
||||
|
||||
- 播放失败设置重试次数10次,解决设备失联后无限重试的问题
|
||||
- 修复最近新增歌单问题
|
||||
- 小程序码移动端布局兼容 (#293)
|
||||
|
||||
## v0.3.55 (2024-12-04)
|
||||
|
||||
### Fix
|
||||
|
||||
- 修复播放接口报错问题
|
||||
|
||||
## v0.3.54 (2024-12-04)
|
||||
|
||||
### Feat
|
||||
|
||||
- 新增最近新增歌单 close #273
|
||||
|
||||
### Fix
|
||||
|
||||
- 安卓低版本webview对audio的src为空值的报错 (#289)
|
||||
- 修复M01语音播放问题,X08C X08E X8F 型号默认采用型号兼容模式 see #30
|
||||
|
||||
## v0.3.53 (2024-12-03)
|
||||
|
||||
### Fix
|
||||
|
||||
- 解决播放接口修改后播放失败的问题
|
||||
|
||||
## v0.3.52 (2024-12-03)
|
||||
|
||||
### Fix
|
||||
|
||||
- 修复播放接口参数错误的问题
|
||||
|
||||
## v0.3.51 (2024-12-03)
|
||||
|
||||
### Fix
|
||||
|
||||
- 修复空配置启动失败问题 close #284
|
||||
|
||||
## v0.3.50 (2024-12-03)
|
||||
|
||||
### Feat
|
||||
|
||||
- 修改日志文件的默认值
|
||||
- 新增修改tag缓存信息的接口 close #266
|
||||
- 新增专用的播放歌曲和播放歌单接口,解决默认口令提示词被修改了导致后台失效的问题
|
||||
- 统计设备型号
|
||||
- 页面与设置中的HOST不一致时弹窗提醒 (#281)
|
||||
- 未发现小爱设备时给予提示 (#278)
|
||||
- 优化设置页面提示
|
||||
|
||||
### Fix
|
||||
|
||||
- 更新 yt-dlp ,解决 B 站下载问题 close #279
|
||||
- 网页播放audio获取到错误url无法播放时提醒用户 (#280)
|
||||
- input标签自闭合
|
||||
|
||||
### Refactor
|
||||
|
||||
- 调整设置页面
|
||||
|
||||
## v0.3.49 (2024-11-28)
|
||||
|
||||
### Feat
|
||||
|
||||
12
Dockerfile
12
Dockerfile
@@ -11,15 +11,19 @@ RUN pdm install --prod --no-editable
|
||||
|
||||
FROM hanxi/xiaomusic:runtime
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/.venv /app/.venv
|
||||
COPY --from=builder /app/.venv ./.venv
|
||||
COPY --from=builder /app/xiaomusic/ ./xiaomusic/
|
||||
COPY --from=builder /app/plugins/ ./plugins/
|
||||
COPY --from=builder /app/xiaomusic.py .
|
||||
ENV XIAOMUSIC_HOSTNAME=192.168.2.5
|
||||
ENV XIAOMUSIC_PORT=8090
|
||||
COPY --from=builder /app/xiaomusic/__init__.py /base_version.py
|
||||
RUN touch /app/.dockerenv
|
||||
|
||||
COPY supervisor.conf /etc/supervisor.conf
|
||||
|
||||
VOLUME /app/conf
|
||||
VOLUME /app/music
|
||||
EXPOSE 8090
|
||||
ENV TZ=Asia/Shanghai
|
||||
ENV PATH=/app/.venv/bin:$PATH
|
||||
ENTRYPOINT [".venv/bin/python3","xiaomusic.py"]
|
||||
|
||||
CMD ["/bin/sh", "-c", "/usr/bin/supervisord -c /etc/supervisor.conf && tail -F /app/supervisord.log /app/xiaomusic.log.txt"]
|
||||
|
||||
@@ -7,6 +7,8 @@ RUN apt-get update && apt-get install -y \
|
||||
libtiff6 \
|
||||
libopenjp2-7 \
|
||||
libxcb1 \
|
||||
supervisor \
|
||||
vim \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
130
README.md
130
README.md
@@ -1,4 +1,5 @@
|
||||
# XiaoMusic: 无限听歌,解放小爱音箱
|
||||
|
||||
[](https://github.com/hanxi/xiaomusic)
|
||||
[](https://hub.docker.com/r/hanxi/xiaomusic)
|
||||
[](https://hub.docker.com/r/hanxi/xiaomusic)
|
||||
@@ -6,8 +7,8 @@
|
||||
[](https://pypi.org/project/xiaomusic/)
|
||||
[](https://pypi.org/project/xiaomusic/)
|
||||
[](https://github.com/hanxi/xiaomusic/releases)
|
||||
|
||||
|
||||
[](https://visitorbadge.io/status?path=hanxi%2Fxiaomusic)
|
||||
[](https://visitorbadge.io/status?path=hanxi%2Fxiaomusic)
|
||||
|
||||
使用小爱音箱播放音乐,音乐使用 yt-dlp 下载。
|
||||
|
||||
@@ -21,13 +22,13 @@
|
||||
已经支持在 web 页面配置其他参数,docker 启动命令如下:
|
||||
|
||||
```bash
|
||||
docker run -p 8090:8090 -v /xiaomusic/music:/app/music -v /xiaomusic/conf:/app/conf hanxi/xiaomusic
|
||||
docker run -p 58090:8090 -e XIAOMUSIC_PUBLIC_PORT=58090 -v /xiaomusic_music:/app/music -v /xiaomusic_conf:/app/conf hanxi/xiaomusic
|
||||
```
|
||||
|
||||
🔥 国内:
|
||||
|
||||
```bash
|
||||
docker run -p 8090:8090 -v /xiaomusic/music:/app/music -v /xiaomusic/conf:/app/conf m.daocloud.io/docker.io/hanxi/xiaomusic
|
||||
docker run -p 58090:8090 -e XIAOMUSIC_PUBLIC_PORT=58090 -v /xiaomusic_music:/app/music -v /xiaomusic_conf:/app/conf m.daocloud.io/docker.io/hanxi/xiaomusic
|
||||
```
|
||||
|
||||
对应的 docker compose 配置如下:
|
||||
@@ -39,10 +40,12 @@ services:
|
||||
container_name: xiaomusic
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 8090:8090
|
||||
- 58090:8090
|
||||
environment:
|
||||
XIAOMUSIC_PUBLIC_PORT: 58090
|
||||
volumes:
|
||||
- /xiaomusic/music:/app/music
|
||||
- /xiaomusic/conf:/app/conf
|
||||
- /xiaomusic_music:/app/music
|
||||
- /xiaomusic_conf:/app/conf
|
||||
```
|
||||
|
||||
🔥 国内:
|
||||
@@ -54,48 +57,34 @@ services:
|
||||
container_name: xiaomusic
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 8090:8090
|
||||
- 58090:8090
|
||||
environment:
|
||||
XIAOMUSIC_PUBLIC_PORT: 58090
|
||||
volumes:
|
||||
- /xiaomusic/music:/app/music
|
||||
- /xiaomusic/conf:/app/conf
|
||||
- /xiaomusic_music:/app/music
|
||||
- /xiaomusic_conf:/app/conf
|
||||
```
|
||||
|
||||
其中 conf 目录为配置文件存放目录,music 目录为音乐存放目录,建议分开配置为不同的目录。
|
||||
- 其中 conf 目录为配置文件存放目录,music 目录为音乐存放目录,建议分开配置为不同的目录。
|
||||
- /xiaomusic_music 和 /xiaomusic_conf 是 docker 主机里的目录,可以修改为其他目录。如果报错找不到 /xiaomusic_music 目录,可以先执行 `mkdir -p /xiaomusic_{music,conf}` 命令新建目录。
|
||||
- XIAOMUSIC_PUBLIC_PORT 是用来配置 NAS 本地端口的。8090 是容器端口,不要去修改。
|
||||
- 后台访问地址为: http://NAS_IP:58090
|
||||
|
||||
> [!NOTE]
|
||||
> 上面配置的 /xiaomusic/music 和 /xiaomusic/conf 是 docker 主机里的 /xiaomusic 目录下的,可以修改为其他目录。如果报错找不到 /xiaomusic/music 目录,可以先执行 `mkdir -p /xiaomusic/{music,conf}` 命令新建目录。
|
||||
|
||||
docker 和 docker compose 二选一即可,启动成功后,在 web 页面可以配置其他参数,带有 `*` 号的配置是必须要配置的,其他的用不上时不用修改。初次配置时需要在页面上输入小米账号和密码保存后才能获取到设备列表。
|
||||
> docker 和 docker compose 二选一即可,启动成功后,在 web 页面可以配置其他参数,带有 `*` 号的配置是必须要配置的,其他的用不上时不用修改。初次配置时需要在页面上输入小米账号和密码保存后才能获取到设备列表。
|
||||
|
||||
> [!TIP]
|
||||
> 目前安装步骤已经是最简化了,如果还是嫌安装麻烦,可以微信或者 QQ 约我远程安装,我一般周末和晚上才有时间,收个辛苦费 :moneybag: 50 元一次,安装失败不收费。
|
||||
|
||||
### 🔥 修改默认8090端口映射
|
||||
|
||||
如果需要修改 8090 端口为其他端口,比如 5678,需要这样配,3个数字都需要是 5678 。见 <https://github.com/hanxi/xiaomusic/issues/19>
|
||||
|
||||
```yaml
|
||||
services:
|
||||
xiaomusic:
|
||||
image: hanxi/xiaomusic
|
||||
container_name: xiaomusic
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 5678:5678
|
||||
volumes:
|
||||
- /xiaomusic/music:/app/music
|
||||
- /xiaomusic/conf:/app/conf
|
||||
environment:
|
||||
XIAOMUSIC_PORT: 5678
|
||||
```
|
||||
|
||||
如果不是首次修改端口,还需要修改 /xiaomusic/conf/setting.json 文件里的端口(也可以在后台修改监听端口后重启)。
|
||||
|
||||
遇到问题可以去 web 设置页面底部点击【下载日志文件】按钮,然后搜索一下日志文件内容确保里面没有账号密码信息后(有就删除这些敏感信息),然后在提 issues 反馈问题时把下载的日志文件带上。
|
||||
|
||||
> [!IMPORTANT]
|
||||
> XIAOMUSIC_PORT 也可以在后台配置,对应的是监听端口。
|
||||
|
||||
> [!TIP]
|
||||
> 海外 RackNerd VPS 机器推荐,可支付宝付款。
|
||||
> - [🔥1 GB KVM VPS $11.29/年](https://my.racknerd.com/aff.php?aff=1177&pid=903)
|
||||
> - [2 GB KVM VPS](https://my.racknerd.com/aff.php?aff=1177&pid=904)
|
||||
> - [3.5 GB KVM VPS](https://my.racknerd.com/aff.php?aff=1177&pid=905)
|
||||
> - [4 GB KVM VPS](https://my.racknerd.com/aff.php?aff=1177&pid=906)
|
||||
> - [6 GB KVM VPS](https://my.racknerd.com/aff.php?aff=1177&pid=907)
|
||||
|
||||
### 🤐 支持语音口令
|
||||
|
||||
@@ -114,7 +103,8 @@ services:
|
||||
- 【播放列表收藏】,这个用于播放收藏歌单。
|
||||
- 【播放本地歌曲+歌名】,这个口令和播放歌曲的区别是本地找不到也不会去下载。
|
||||
- 【播放列表第几个+列表名】,具体见: <https://github.com/hanxi/xiaomusic/issues/158>
|
||||
- 【播放歌曲+关键词】,会搜索关键词作为临时搜索列表播放,比如说【播放歌曲林俊杰】,会播放所有林俊杰的歌。
|
||||
- 【搜索播放+关键词】,会搜索关键词作为临时搜索列表播放,比如说【搜索播放林俊杰】,会播放所有林俊杰的歌。
|
||||
- 【本地搜索播放+关键词】,跟搜索播放的区别是本地找不到也不会去下载。
|
||||
|
||||
> [!TIP]
|
||||
> 隐藏玩法: 对小爱同学说播放歌曲小猪佩奇的故事,会先下载小猪佩奇的故事,然后再播放小猪佩奇的故事。
|
||||
@@ -129,7 +119,7 @@ services:
|
||||
\ / | | / _` | / _ \ | |\/| | | | | | / __| | | / __|
|
||||
/ \ | | | (_| | | (_) | | | | | | |_| | \__ \ | | | (__
|
||||
/_/\_\ |_| \__,_| \___/ |_| |_| \__,_| |___/ |_| \___|
|
||||
XiaoMusic v0.3.37 by: github.com/hanxi
|
||||
XiaoMusic v0.3.65 by: github.com/hanxi
|
||||
|
||||
usage: xiaomusic [-h] [--port PORT] [--hardware HARDWARE] [--account ACCOUNT]
|
||||
[--password PASSWORD] [--cookie COOKIE] [--verbose]
|
||||
@@ -171,8 +161,7 @@ pdm run xiaomusic.py
|
||||
提交前请执行
|
||||
|
||||
```
|
||||
pdm fmt
|
||||
pdm lint --fix
|
||||
pdm lintfmt
|
||||
```
|
||||
|
||||
用于检查代码和格式化代码。
|
||||
@@ -183,6 +172,13 @@ pdm lint --fix
|
||||
docker build -t xiaomusic .
|
||||
```
|
||||
|
||||
### 技术栈
|
||||
|
||||
- 后端代码使用 Python 语言编写。
|
||||
- HTTP 服务使用的是 FastAPI 框架,~~早期版本使用的是 Flask~~。
|
||||
- 使用了 Docker ,在 NAS 上安装更方便。
|
||||
- 默认的前端主题使用了 jQuery 。
|
||||
|
||||
## 已测试支持的设备
|
||||
|
||||
| 型号 | 名称 |
|
||||
@@ -201,7 +197,8 @@ docker build -t xiaomusic .
|
||||
| L05C | [小米小爱音箱Play 增强版](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l05c) |
|
||||
| L09A | [小米音箱Art](https://home.mi.com/webapp/content/baike/product/index.html?model=xiaomi.wifispeaker.l09a) |
|
||||
| LX04 X10A X08A | 已经支持的触屏版 |
|
||||
| M01/XMYX01JY | 小米小爱音箱HD (获取对话记录的接口比较特殊) |
|
||||
| X08C X08E X8F | 需要设置【型号兼容模式】选项为 true |
|
||||
| M01/XMYX01JY | 小米小爱音箱HD 需要设置【特殊型号获取对话记录】选项为 true 才能语音播放|
|
||||
|
||||
型号与产品名称对照可以在这里查询 <https://home.miot-spec.com/s/xiaomi.wifispeaker>
|
||||
|
||||
@@ -224,24 +221,6 @@ docker build -t xiaomusic .
|
||||
> 已知 L05B L05C LX06 L16A 不支持 flac 格式。
|
||||
> 如果格式不能播放可以打开【转换为MP3】和【型号兼容模式】选项。具体见 <https://github.com/hanxi/xiaomusic/issues/153#issuecomment-2328168689>
|
||||
|
||||
|
||||
## 💡 简易的控制面板
|
||||
|
||||
浏览器进入 <http://192.168.2.5:8090>
|
||||
|
||||
- ip 是 XIAOMUSIC_HOSTNAME 设置的
|
||||
- 8090 是默认端口
|
||||
- 支持功能
|
||||
- 显示正在播放的歌曲
|
||||
- 模糊搜索本地歌曲
|
||||
- 播放列表
|
||||
- 删除歌曲
|
||||
- 设置页面
|
||||
- 配置网络歌单
|
||||
- 日志文件下载
|
||||
|
||||
采用新的设置页面之后,没有必须在启动前配置的环境变量了,除非是改默认的 8090 端口才需要配置环境变量。
|
||||
|
||||
## 🌏 网络歌单功能
|
||||
|
||||
可以配置一个 json 格式的歌单,支持电台和歌曲,也可以直接用别人分享的链接,同时配备了 m3u 文件格式转换工具,可以很方便的把 m3u 电台文件转换成网络歌单格式的 json 文件,具体用法见 <https://github.com/hanxi/xiaomusic/issues/78>
|
||||
@@ -251,30 +230,14 @@ docker build -t xiaomusic .
|
||||
|
||||
## 🍺 更多其他可选配置
|
||||
|
||||
- XIAOMUSIC_ACTIVE_CMD 环境变量,对应后台的 【允许唤醒的命令】,用于唤醒口令,配置成'play,random_play',在非播放状态下,只有这两个指令(播放歌曲和随机播放)可以触发,触发后,xiaomusic进入playing状态,其他指令则可以正常触发。具体见 <https://github.com/hanxi/xiaomusic/pull/43>
|
||||
- XIAOMUSIC_EXCLUDE_DIRS 配置歌曲目录里需要忽略的目录,对应后台的 【忽略目录】
|
||||
- XIAOMUSIC_MUSIC_PATH_DEPTH 配置歌曲目录搜索深度,对应后台的 【目录深度】,具体见 <https://github.com/hanxi/xiaomusic/issues/76>
|
||||
- XIAOMUSIC_DISABLE_HTTPAUTH 配置成 false 表示开启密码访问web控制台,对应后台的 【关闭密码验证】,具体见 <https://github.com/hanxi/xiaomusic/issues/47>
|
||||
- XIAOMUSIC_HTTPAUTH_USERNAME 配置 web 控制台用户,对应后台的 【控制台账户】
|
||||
- XIAOMUSIC_HTTPAUTH_PASSWORD 配置 web 控制台密码,对应后台的 【控制台密码】
|
||||
- XIAOMUSIC_CONF_PATH 用来存放配置文件的目录,对应后台的 【配置文件目录】,记得把目录映射到主机,默认为 `/app/config` ,具体见 <https://github.com/hanxi/xiaomusic/issues/74>
|
||||
- XIAOMUSIC_CACHE_DIR 用来音乐 tag 缓存,默认为 `/app/cache`,对应后台的 【缓存文件目录】。
|
||||
- XIAOMUSIC_DISABLE_DOWNLOAD 设为 true 时关闭下载功能,对应后台的 【关闭下载功能】,见 <https://github.com/hanxi/xiaomusic/issues/82>
|
||||
- XIAOMUSIC_USE_MUSIC_API 设为 true 时使用 player_play_music 接口播放音乐,对应后台的 【型号兼容模式】,用于兼容不能播放的型号,如果发现需要设置这个选项的时候请告知我加一下设备型号,方便以后不用设置。 见 <https://github.com/hanxi/xiaomusic/issues/30>
|
||||
- XIAOMUSIC_KEYWORDS_PLAY 用来播放歌曲的口令前缀,对应后台的 【播放歌曲口令】,默认是 "播放歌曲,放歌曲" ,可以用英文逗号分割配置多个
|
||||
- XIAOMUSIC_KEYWORDS_STOP 用来关机的口令,对应后台的 【停止口令】,默认是 "关机,暂停,停止" ,可以用英文逗号分割配置多个。
|
||||
- XIAOMUSIC_KEYWORDS_PLAYLOCAL 用来播放本地歌曲的口令前缀,对应后台的 【播放本地歌曲口令】,本地找不到时不会下载歌曲,默认是 "播放本地歌曲,本地播放歌曲" ,可以用英文逗号分割配置多个。
|
||||
- XIAOMUSIC_ENABLE_FUZZY_MATCH 设为 true 时开启模糊匹配(默认),设为 false 时关闭模糊匹配,对应后台的 【开启模糊搜索】,支持模糊匹配歌名和歌单名。 具体见 <https://github.com/hanxi/xiaomusic/issues/52>
|
||||
- XIAOMUSIC_FUZZY_MATCH_CUTOFF 设置模糊搜索匹配的最低相似度阈值(默认0.6,可以配0到1直接的小数),越小越模糊,越大越精准,对应后台的 【模糊匹配阈值】。具体见 <https://github.com/hanxi/xiaomusic/issues/52>
|
||||
- XIAOMUSIC_PUBLIC_PORT 用于设置外网端口,对应后台的 【外网访问端口】,当使用反向代理时可以设置为外网端口,XIAOMUSIC_HOSTNAME 设为外网IP或者域名即可。
|
||||
- XIAOMUSIC_DOWNLOAD_PATH 变量可以配置下载目录,默认为空,表示使用 music 目录为下载目录,对应后台的 【音乐下载目录】。设置这个目录必须是 music 的子目录,否则刷新列表后会找不到歌曲。具体见 <https://github.com/hanxi/xiaomusic/issues/98>
|
||||
- XIAOMUSIC_PROXY 用于配置国内使用 youtube 源下载歌曲时使用的代理,参数格式参考 yt-dlp 文档说明。 见 <https://github.com/hanxi/xiaomusic/issues/2> 和 <https://github.com/hanxi/xiaomusic/issues/11>
|
||||
- MIIO_TTS_CMD 用于部分机型(如:`L05C`)使用 MiIO 支持 tts 能力,默认为空,命令选择见 [MiService-fork 文档](https://github.com/yihong0618/MiService)
|
||||
见 <https://github.com/hanxi/xiaomusic/issues/333>
|
||||
|
||||
### ⚠️ 安全提醒
|
||||
## ⚠️ 安全提醒
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 如果配置了公网访问 xiaomusic ,请一定要开启密码登陆,并设置复杂的密码。且不要在公共场所的 WiFi 环境下使用,否则可能造成小米账号密码泄露。
|
||||
>
|
||||
> 1. 如果配置了公网访问 xiaomusic ,请一定要开启密码登陆,并设置复杂的密码。且不要在公共场所的 WiFi 环境下使用,否则可能造成小米账号密码泄露。
|
||||
> 2. 强烈不建议将小爱音箱的小米账号绑定摄像头,代码难免会有 bug ,一旦小米账号密码泄露,可能监控录像也会泄露。
|
||||
|
||||
## 🤔 高级篇
|
||||
|
||||
@@ -301,6 +264,8 @@ docker build -t xiaomusic .
|
||||
- [pure 主题 xiaomusicUI](https://github.com/52fisher/xiaomusicUI)
|
||||
- [移动端的播放器主题](https://github.com/52fisher/XMusicPlayer)
|
||||
- [一个第三方的主题](https://github.com/DarrenWen/xiaomusicui)
|
||||
- [Umami 统计](https://github.com/umami-software/umami)
|
||||
- [Sentry 报错监控](https://github.com/getsentry/sentry)
|
||||
- 所有帮忙调试和测试的朋友
|
||||
- 所有反馈问题和建议的朋友
|
||||
|
||||
@@ -329,4 +294,3 @@ docker build -t xiaomusic .
|
||||
## License
|
||||
|
||||
[MIT](https://github.com/hanxi/xiaomusic/blob/main/LICENSE) License © 2024 涵曦
|
||||
|
||||
|
||||
70
docs/.vitepress/config.mts
Normal file
70
docs/.vitepress/config.mts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { loadEnv, defineConfig } from 'vitepress'
|
||||
import AutoSidebar from 'vite-plugin-vitepress-auto-sidebar';
|
||||
import GitHubIssuesPlugin from './vitepress-plugin-github-issues.mts';
|
||||
|
||||
export default async ({ mode }) => {
|
||||
const env = loadEnv(mode || '', process.cwd())
|
||||
return defineConfig({
|
||||
title: "XiaoMusic",
|
||||
description: "XiaoMusic doc",
|
||||
themeConfig: {
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
nav: [
|
||||
{ text: 'Guide', link: '/issues' },
|
||||
{ text: 'Admin', link: 'https://x.hanxi.cc' },
|
||||
],
|
||||
|
||||
socialLinks: [
|
||||
{ icon: 'github', link: 'https://github.com/hanxi/xiaomusic' }
|
||||
],
|
||||
|
||||
footer: {
|
||||
message: '基于 MIT 许可发布',
|
||||
copyright: `版权所有 © 2023-${new Date().getFullYear()} 涵曦`
|
||||
},
|
||||
},
|
||||
sitemap: {
|
||||
hostname: 'https://xdocs.hanxi.cc'
|
||||
},
|
||||
head: [
|
||||
['script', { defer: true, src: 'https://umami.hanxi.cc/script.js', 'data-website-id': '29cca3f5-e420-432b-adc7-8a1325d31c68' }]
|
||||
],
|
||||
lastUpdated: true,
|
||||
markdown: {
|
||||
lineNumbers: false, // 关闭代码块行号显示
|
||||
// 自定义 markdown-it 插件
|
||||
config: (md) => {
|
||||
md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
|
||||
const aIndex = tokens[idx].attrIndex('target');
|
||||
if (aIndex < 0) {
|
||||
tokens[idx].attrPush(['target', '_self']); // 将默认行为改为不使用 _blank
|
||||
} else {
|
||||
tokens[idx].attrs![aIndex][1] = '_self'; // 替换 _blank 为 _self
|
||||
}
|
||||
return self.renderToken(tokens, idx, options);
|
||||
};
|
||||
},
|
||||
},
|
||||
logLevel: 'warn',
|
||||
vite: {
|
||||
plugins: [
|
||||
AutoSidebar({
|
||||
path: '.',
|
||||
collapsed: true,
|
||||
titleFromFile: true,
|
||||
}),
|
||||
GitHubIssuesPlugin({
|
||||
repo: 'hanxi/xiaomusic',
|
||||
token: env.VITE_GITHUB_ISSUES_TOKEN,
|
||||
replaceRules: [
|
||||
{
|
||||
baseUrl: 'https://github.com/hanxi/xiaomusic/issues',
|
||||
targetUrl: '/issues',
|
||||
},
|
||||
],
|
||||
githubProxy: 'https://gproxy.hanxi.cc/proxy',
|
||||
}),
|
||||
],
|
||||
}
|
||||
})
|
||||
}
|
||||
259
docs/.vitepress/vitepress-plugin-github-issues.mts
Normal file
259
docs/.vitepress/vitepress-plugin-github-issues.mts
Normal file
@@ -0,0 +1,259 @@
|
||||
// vitepress-plugin-github-issues.mts
|
||||
|
||||
import axios from 'axios';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import type { Plugin } from 'vitepress';
|
||||
|
||||
interface ReplaceRule {
|
||||
baseUrl: string; // 要匹配的基地址
|
||||
targetUrl: string; // 替换后的目标地址
|
||||
}
|
||||
|
||||
interface GitHubIssuesPluginOptions {
|
||||
repo: string; // GitHub repository info in the format 'owner/repo'
|
||||
token: string;
|
||||
replaceRules: ReplaceRule[];
|
||||
githubProxy: string;
|
||||
}
|
||||
|
||||
async function fetchAllIssues(repo: string, token: string): Promise<any[]> {
|
||||
const maxRetries = 3; // 最大重试次数
|
||||
let attempt = 0;
|
||||
const allIssues: any[] = [];
|
||||
let page = 1;
|
||||
|
||||
while (true) {
|
||||
while (attempt < maxRetries) {
|
||||
try {
|
||||
const response = await axios.get(`https://api.github.com/repos/${repo}/issues`, {
|
||||
headers: {
|
||||
Authorization: `token ${token}`
|
||||
},
|
||||
params: {
|
||||
page: page,
|
||||
per_page: 100 // 每页最多返回100条记录
|
||||
}
|
||||
});
|
||||
|
||||
// 如果没有更多问题了,退出循环
|
||||
if (response.data.length === 0) {
|
||||
return allIssues;
|
||||
}
|
||||
|
||||
allIssues.push(...response.data);
|
||||
page++; // 下一页
|
||||
attempt = 0; // 重置尝试次数
|
||||
break; // 退出尝试循环
|
||||
} catch (error) {
|
||||
if (error.response && error.response.status === 503) {
|
||||
console.error(`服务不可用, 正在重试...`);
|
||||
attempt++;
|
||||
const waitTime = Math.pow(2, attempt) * 1000; // 指数等待时间
|
||||
await new Promise(resolve => setTimeout(resolve, waitTime));
|
||||
} else {
|
||||
throw error; // 如果不是503错误,抛出错误并停止重试
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (attempt >= maxRetries) {
|
||||
throw new Error('最大重试次数已达,请检查 API 状态(可能是请求过于频繁)');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchIssueComments(repo: string, issueNumber: number, token: string): Promise<any[]> {
|
||||
const maxRetries = 3;
|
||||
let attempt = 0;
|
||||
const allComments: any[] = [];
|
||||
let page = 1;
|
||||
|
||||
while (true) {
|
||||
while (attempt < maxRetries) {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
`https://api.github.com/repos/${repo}/issues/${issueNumber}/comments`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `token ${token}`,
|
||||
},
|
||||
params: {
|
||||
page: page,
|
||||
per_page: 100,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (response.data.length === 0) {
|
||||
return allComments; // 如果没有更多评论,退出循环
|
||||
}
|
||||
|
||||
allComments.push(...response.data);
|
||||
page++;
|
||||
attempt = 0;
|
||||
break; // 成功获取评论数据,退出重试
|
||||
} catch (error: any) {
|
||||
if (error.response && error.response.status === 503) {
|
||||
console.error('服务不可用,正在重试...');
|
||||
attempt++;
|
||||
const waitTime = Math.pow(2, attempt) * 1000;
|
||||
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (attempt >= maxRetries) {
|
||||
throw new Error('最大重试次数已达,请检查 API 状态(可能是请求过于频繁)');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearDirectory(dir: string) {
|
||||
if (fs.existsSync(dir)) {
|
||||
fs.readdirSync(dir).forEach((file) => {
|
||||
const filePath = path.join(dir, file);
|
||||
if (fs.lstatSync(filePath).isDirectory()) {
|
||||
clearDirectory(filePath); // 递归清理子目录
|
||||
fs.rmdirSync(filePath);
|
||||
} else {
|
||||
fs.unlinkSync(filePath); // 删除文件
|
||||
}
|
||||
});
|
||||
console.log(`Cleared directory: ${dir}`);
|
||||
}
|
||||
}
|
||||
|
||||
function copyFile(source: string, destination: string) {
|
||||
if (fs.existsSync(source)) {
|
||||
fs.copyFileSync(source, destination);
|
||||
console.log(`Copied file from ${source} to ${destination}`);
|
||||
} else {
|
||||
console.error(`file not found at ${source}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 在文件开头插入内容
|
||||
function prependToFile(filePath: string, text: string) {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const updatedContent = `${text}\n\n${content}`;
|
||||
fs.writeFileSync(filePath, updatedContent, 'utf-8');
|
||||
console.log(`Prepended text to ${filePath}`);
|
||||
}
|
||||
|
||||
function replaceGithubAssetUrls(content: string, githubProxy: string): string {
|
||||
const pattern1 = /https:\/\/github\.com\/[^\/]+\/[^\/]+\/assets\/[\w-]+/g;
|
||||
const pattern2 = /https:\/\/github\.com\/user-attachments\/assets\/[\w-]+/g;
|
||||
|
||||
// 使用正则表达式替换符合条件的链接
|
||||
const transformedContent = content.replace(pattern1, (match) => {
|
||||
return match.replace("https://github.com", githubProxy);
|
||||
}).replace(pattern2, (match) => {
|
||||
return match.replace("https://github.com", githubProxy);
|
||||
});
|
||||
|
||||
return transformedContent;
|
||||
}
|
||||
|
||||
export default function GitHubIssuesPlugin(options: GitHubIssuesPluginOptions): Plugin {
|
||||
const { repo, token, replaceRules, githubProxy } = options;
|
||||
|
||||
return {
|
||||
name: 'vitepress-plugin-github-issues',
|
||||
|
||||
async buildStart() {
|
||||
try {
|
||||
const issues = await fetchAllIssues(repo, token);
|
||||
|
||||
console.log(`Fetched ${issues.length} issues from GitHub`); // Log the number of issues fetched
|
||||
|
||||
const docsDir = path.join(process.cwd(), 'issues');
|
||||
|
||||
// 清空 issues 目录
|
||||
clearDirectory(docsDir);
|
||||
|
||||
// Create a directory to store markdown files if it doesn't exist
|
||||
if (!fs.existsSync(docsDir)) {
|
||||
fs.mkdirSync(docsDir);
|
||||
console.log(`Created docs directory: ${docsDir}`);
|
||||
}
|
||||
|
||||
// 拷贝 ../README.md 文件到当前目录
|
||||
const readmeSource = path.join(process.cwd(), '../README.md');
|
||||
const readmeDestination = path.join(docsDir, 'index.md');
|
||||
copyFile(readmeSource, readmeDestination);
|
||||
|
||||
// 拷贝 ../CHANGELOG.md 文件到当前目录
|
||||
const changelogSource = path.join(process.cwd(), '../CHANGELOG.md');
|
||||
const changelogDestination = path.join(docsDir, 'changelog.md');
|
||||
copyFile(changelogSource, changelogDestination);
|
||||
prependToFile(changelogDestination, '# 版本日志');
|
||||
|
||||
for (const issue of issues) {
|
||||
// 仅处理包含 "文档" 标签的 issue
|
||||
const hasDocumentLabel = issue.labels.some(label => label.name === '文档');
|
||||
if (hasDocumentLabel) {
|
||||
const title = issue.title.replace(/[\/\\?%*:|"<>]/g, '-');
|
||||
const fileName = `${issue.number}.md`;
|
||||
|
||||
// 获取评论数据
|
||||
const comments = await fetchIssueComments(repo, issue.number, token);
|
||||
|
||||
let content =
|
||||
`---
|
||||
title: ${issue.title}
|
||||
---
|
||||
|
||||
# ${title}
|
||||
|
||||
${issue.body}
|
||||
|
||||
## 评论
|
||||
|
||||
`;
|
||||
|
||||
// 插入评论
|
||||
if (comments.length > 0) {
|
||||
comments.forEach((comment, index) => {
|
||||
content += `
|
||||
### 评论 ${index + 1} - ${comment.user.login}
|
||||
|
||||
${comment.body}
|
||||
|
||||
---
|
||||
`;
|
||||
});
|
||||
} else {
|
||||
content += "没有评论。\n";
|
||||
}
|
||||
|
||||
replaceRules.forEach(({ baseUrl, targetUrl }) => {
|
||||
// 将 baseUrl 转换为正则表达式,匹配后接的路径部分
|
||||
const pattern = new RegExp(`${baseUrl.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')}(/\\d+)`, 'g');
|
||||
// 替换为目标 URL
|
||||
content = content.replace(pattern, `${targetUrl}$1.html`);
|
||||
});
|
||||
|
||||
content = replaceGithubAssetUrls(content, githubProxy);
|
||||
|
||||
content += `[链接到 GitHub Issue](${issue.html_url})\n`;
|
||||
|
||||
const filePath = path.join(docsDir, fileName);
|
||||
|
||||
fs.writeFileSync(filePath, content, { encoding: 'utf8' });
|
||||
console.log(`Created file: ${filePath}`); // Log each created file
|
||||
} else {
|
||||
console.log(`Skipped issue: ${issue.title}`); // Log skipped issues
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Successfully created markdown files from GitHub issues.`);
|
||||
} catch (error) {
|
||||
console.error('Error fetching GitHub issues:', error);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
1
docs/CNAME
Normal file
1
docs/CNAME
Normal file
@@ -0,0 +1 @@
|
||||
xdocs.hanxi.cc
|
||||
28
docs/index.md
Normal file
28
docs/index.md
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
# https://vitepress.dev/reference/default-theme-home-page
|
||||
layout: home
|
||||
|
||||
hero:
|
||||
name: "XiaoMusic"
|
||||
text: "无限听歌<br>解放小爱音箱"
|
||||
tagline: 使用小爱音箱播放音乐,音乐使用 yt-dlp 下载
|
||||
actions:
|
||||
- theme: brand
|
||||
text: 快速开始
|
||||
link: /issues/index
|
||||
- theme: alt
|
||||
text: 文档汇总
|
||||
link: /issues/211
|
||||
- theme: alt
|
||||
text: GitHub
|
||||
link: https://github.com/hanxi/xiaomusic
|
||||
|
||||
features:
|
||||
- title: MIT 开源
|
||||
details: 完全开源,自主可控
|
||||
- title: 一键部署
|
||||
details: 支持 Docker 部署,兼容各大 NAS 平台
|
||||
- title: 口令自定义
|
||||
details: 可以完全自定义语音口令,可以写自己的插件
|
||||
---
|
||||
|
||||
85
docs/issues/101.md
Normal file
85
docs/issues/101.md
Normal file
@@ -0,0 +1,85 @@
|
||||
---
|
||||
title: 群晖docker安装 xiaomusic
|
||||
---
|
||||
|
||||
# 群晖docker安装 xiaomusic
|
||||
|
||||
由于现在群晖已经无法正常下载 docker 里的镜像了,绕了好多弯;现在用 ssh 服务命令拉取镜像来创建容器;
|
||||
|
||||
## 1. ssh 输入账号密码进入群晖
|
||||
## 2. 输入 sudo -i 再次输入密码进入 root 权限
|
||||
## 3. 输入 docker search xiaomusic来查找到该镜像名;
|
||||
## 4. 然后输入 docker pull xiaomusic 试试是否能安装,如果不能;就得在命令前加个代理地址;下面列了一些代理地址可以一个个的试
|
||||
|
||||
```
|
||||
docker.fxxk.dedyn.iodocker.io
|
||||
registry-docker-hub-latest-9vgc.onrender.com
|
||||
docker.chenby.cn
|
||||
dockerproxy.com
|
||||
hub.uuuadc.top
|
||||
docker.jsdelivr.fyi
|
||||
docker.registry.cyou
|
||||
dockerhub.anzu.vip
|
||||
```
|
||||
|
||||
我是用的最下面这个成功的
|
||||
|
||||
```
|
||||
docker pull dockerhub.anzu.vip/xiaomusic:latest
|
||||
```
|
||||
|
||||
## 5. 安装完成后就进入群晖 DOCKER 配置 xiaomusic
|
||||
|
||||
<img width="491" alt="image" src="https://gproxy.hanxi.cc/proxy/hanxi/xiaomusic/assets/38914725/e318062b-bd70-464c-a8df-8ce3635f2d84">
|
||||
|
||||
- MI_HARDWARE=型号 前面第4 步骤获取的
|
||||
- XIAOMUSIC_SEARCH=搜索方式,我填写的bilisearch: 意思是通过 bilibili 搜索
|
||||
- MI_DID=前面第4 步骤获取的
|
||||
- MI_USER=小米账号
|
||||
- MI_PASS=小米密码
|
||||
- XIAOMUSIC_FUZZY_MATCH_CUTOFF=模糊匹配,最小为 0.1 最大为 1,越小越模糊,越大越精准
|
||||
|
||||
## 6. 配置端口
|
||||
|
||||
<img width="757" alt="image (1)" src="https://gproxy.hanxi.cc/proxy/hanxi/xiaomusic/assets/38914725/2b6b9283-296f-4845-a3ff-0ebb11f548b4">
|
||||
|
||||
## 7. 映射路径
|
||||
|
||||
<img width="737" alt="image (2)" src="https://gproxy.hanxi.cc/proxy/hanxi/xiaomusic/assets/38914725/593718dd-8302-4a69-bec9-36e70f3f0407">
|
||||
|
||||
|
||||
|
||||
## 评论
|
||||
|
||||
|
||||
### 评论 1 - kiwi5656
|
||||
|
||||
MI_DID=前面第4 步骤获取的,第4步骤在哪?
|
||||
|
||||
---
|
||||
|
||||
### 评论 2 - hanxi
|
||||
|
||||
> MI_DID=前面第4 步骤获取的,第4步骤在哪?
|
||||
|
||||
不用设置 MI_DID
|
||||
|
||||
---
|
||||
|
||||
### 评论 3 - hanxi
|
||||
|
||||
国内 docker 镜像
|
||||
|
||||
```
|
||||
docker pull m.daocloud.io/docker.io/hanxi/xiaomusic:latest
|
||||
docker tag m.daocloud.io/docker.io/hanxi/xiaomusic:latest hanxi/xiaomusic:latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 评论 4 - ginitaimeiyty
|
||||
|
||||
如果手头上有能科学上网的机器,直接把群辉的代理服务器IP填写成可以科学上网的机器IP+端口,翻墙软件打开允许局域网连接就可以
|
||||
|
||||
---
|
||||
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/101)
|
||||
404
docs/issues/105.md
Normal file
404
docs/issues/105.md
Normal file
@@ -0,0 +1,404 @@
|
||||
---
|
||||
title: 【插件】自定义口令功能
|
||||
---
|
||||
|
||||
# 【插件】自定义口令功能
|
||||
|
||||
自定义口令配置需要配置到 config.json 文件里,使用 config.json 方式启动。参考 </issues/94.html> 。
|
||||
|
||||
口令的配置方式见 config-example.json 文件。口令对应的代码需要写到 `plugins/` 目录下面,如果是容器启动,则需要把这个目录挂载出来。
|
||||
|
||||
config.json 格式是下面这样的。
|
||||
|
||||
```json
|
||||
{
|
||||
"hardware": "L07A",
|
||||
"account": "",
|
||||
"password": "",
|
||||
"mi_did": "",
|
||||
"cookie": "",
|
||||
"verbose": false,
|
||||
"music_path": "music",
|
||||
"conf_path": null,
|
||||
"hostname": "192.168.2.5",
|
||||
"port": 8090,
|
||||
"public_port": 0,
|
||||
"proxy": null,
|
||||
"search_prefix": "bilisearch:",
|
||||
"ffmpeg_location": "./ffmpeg/bin",
|
||||
"active_cmd": "play,random_play,playlocal,play_music_list,stop",
|
||||
"exclude_dirs": "@eaDir",
|
||||
"music_path_depth": 10,
|
||||
"disable_httpauth": true,
|
||||
"httpauth_username": "admin",
|
||||
"httpauth_password": "admin",
|
||||
"music_list_url": "",
|
||||
"music_list_json": "",
|
||||
"disable_download": false,
|
||||
"key_word_dict": {
|
||||
"播放歌曲": "play",
|
||||
"播放本地歌曲": "playlocal",
|
||||
"关机": "stop",
|
||||
"下一首": "play_next",
|
||||
"单曲循环": "set_play_type_one",
|
||||
"全部循环": "set_play_type_all",
|
||||
"随机播放": "random_play",
|
||||
"分钟后关机": "stop_after_minute",
|
||||
"播放列表": "play_music_list",
|
||||
"刷新列表": "gen_music_list",
|
||||
"set_volume#": "set_volume",
|
||||
"get_volume#": "get_volume",
|
||||
"本地播放歌曲": "playlocal",
|
||||
"放歌曲": "play",
|
||||
"暂停": "stop",
|
||||
"停止": "stop",
|
||||
"停止播放": "stop",
|
||||
"测试自定义口令": "exec#code1(\"hello\")",
|
||||
"测试链接": "exec#httpget(\"https://github.com/hanxi/xiaomusic\")"
|
||||
},
|
||||
"key_match_order": [
|
||||
"set_volume#",
|
||||
"get_volume#",
|
||||
"分钟后关机",
|
||||
"播放歌曲",
|
||||
"下一首",
|
||||
"单曲循环",
|
||||
"全部循环",
|
||||
"随机播放",
|
||||
"关机",
|
||||
"刷新列表",
|
||||
"播放列表",
|
||||
"播放本地歌曲",
|
||||
"本地播放歌曲",
|
||||
"放歌曲",
|
||||
"暂停",
|
||||
"停止",
|
||||
"停止播放",
|
||||
"测试自定义口令",
|
||||
"测试链接"
|
||||
],
|
||||
"use_music_api": false,
|
||||
"use_music_audio_id": "1582971365183456177",
|
||||
"use_music_id": "355454500",
|
||||
"log_file": "/tmp/xiaomusic.txt",
|
||||
"fuzzy_match_cutoff": 0.6,
|
||||
"enable_fuzzy_match": true,
|
||||
"stop_tts_msg": "收到,再见",
|
||||
"keywords_playlocal": "播放本地歌曲,本地播放歌曲",
|
||||
"keywords_play": "播放歌曲,放歌曲",
|
||||
"keywords_stop": "关机,暂停,停止,停止播放",
|
||||
"user_key_word_dict": {
|
||||
"测试自定义口令": "exec#code1(\"hello\")",
|
||||
"测试链接": "exec#httpget(\"https://github.com/hanxi/xiaomusic\")"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
配置自定义口令时,只需要配置 user_key_word_dict 即可,会自动插入到 key_word_dict 里的。配置格式是:
|
||||
|
||||
```
|
||||
"测试自定义口令": "exec#code1(\"hello\")",
|
||||
```
|
||||
|
||||
其中 "测试自定义口令" 就是对小爱音箱说的,`"exec#code1(\"hello\")"` 就是要执行的插件代码,代码以 `exec#` 开头,后面紧跟着执行代码。这里 code1 是一个插件函数,插件函数需要在 plugin 目录里实现,一个文件只会导出一个与文件名相同的插件函数。所以 code1 函数是在 plugin/code1.py 里实现的。
|
||||
|
||||
```
|
||||
async def code1(arg1):
|
||||
global log, xiaomusic
|
||||
log.info(f"code1:{arg1}")
|
||||
await xiaomusic.do_tts("你好,我是自定义的测试口令")
|
||||
```
|
||||
|
||||
这里只是演示了打印日志和让小爱音箱说话。还有一个示例插件是 httpget ,可以用来访问 url 。
|
||||
|
||||
比如下面这样配置的话,当对小爱音箱说测试链接时,会去访问 url ,可以用来很多其他的事情。
|
||||
|
||||
```
|
||||
"测试链接": "exec#httpget(\"https://github.com/hanxi/xiaomusic\")
|
||||
```
|
||||
|
||||
最后还需要在 `active_cmd` 中配上口令用于唤醒:
|
||||
|
||||
```
|
||||
"active_cmd": "play,set_random_play,playlocal,play_music_list,play_music_list_index,stop_after_minute,stop,测试自定义口令",
|
||||
```
|
||||
|
||||
感兴趣的可以体验一下,写了有什么好玩的插件也可以在这里分享,或者提 pr 合并进官方库里作为自带插件。
|
||||
|
||||
|
||||
## 评论
|
||||
|
||||
|
||||
### 评论 1 - carson512
|
||||
|
||||
如果开启服务的状态下 如何唤醒才能调用原有的播放QQ 网易云?而特定唤醒词调用xiaoai播放?
|
||||
|
||||
---
|
||||
|
||||
### 评论 2 - hanxi
|
||||
|
||||
> 如果开启服务的状态下 如何唤醒才能调用原有的播放QQ 网易云?而特定唤醒词调用xiaoai播放?
|
||||
|
||||
不使用 xiaomusic 的唤醒词就会调用音箱自带的,比如说播放音乐
|
||||
|
||||
---
|
||||
|
||||
### 评论 3 - shellingford37
|
||||
|
||||
```
|
||||
[23:26:12] [0.3.30] [INFO] xiaomusic.py:531: 收到消息:测试自定义口令 控制面板:False did:290874427
|
||||
[23:26:12] [0.3.30] [INFO] xiaomusic.py:577: 完全匹配指令. query:测试自定义口令 opvalue:exec#code1("hello")
|
||||
[23:26:12] [0.3.30] [INFO] code1.py:3: code1:hello
|
||||
[23:26:12] [0.3.30] [ERROR] xiaomusic.py:542: Execption XiaoMusic.do_tts() missing 1 required positional argument: 'value'
|
||||
Traceback (most recent call last):
|
||||
File "/app/xiaomusic/xiaomusic.py", line 540, in do_check_cmd
|
||||
await func(did=did, arg1=oparg)
|
||||
File "/app/xiaomusic/xiaomusic.py", line 890, in exec
|
||||
await self.plugin_manager.execute_plugin(code)
|
||||
File "/app/xiaomusic/plugin.py", line 66, in execute_plugin
|
||||
await coroutine
|
||||
File "/app/plugins/code1.py", line 4, in code1
|
||||
await xiaomusic.do_tts("你好,我是自定义的测试口令")
|
||||
TypeError: XiaoMusic.do_tts() missing 1 required positional argument: 'value'
|
||||
```
|
||||
|
||||
我用code1的代码执行报错,有大佬知道为什么吗?
|
||||
|
||||
---
|
||||
|
||||
### 评论 4 - hanxi
|
||||
|
||||
@shellingford37 重构后漏改了,修复了。
|
||||
|
||||
---
|
||||
|
||||
### 评论 5 - guoxiangke
|
||||
|
||||
先说播放歌曲,再说 测试自定义口令 就行
|
||||
|
||||
---
|
||||
|
||||
### 评论 6 - CZJCC
|
||||
|
||||
想请教下插件那个功能,如何把用户的语音输入作为参数内容传到自定义函数里
|
||||
|
||||
---
|
||||
|
||||
### 评论 7 - hanxi
|
||||
|
||||
> 想请教下插件那个功能,如何把用户的语音输入作为参数内容传到自定义函数里
|
||||
|
||||
现在获取不到,等我加个接口获取吧。
|
||||
|
||||
---
|
||||
|
||||
### 评论 8 - CZJCC
|
||||
|
||||
666,支持以后我可以贡献一个接入通义模型的插件
|
||||
|
||||
---
|
||||
|
||||
### 评论 9 - hanxi
|
||||
|
||||
@CZJCC 你可以更新看看 plugins/code1.py 的测试代码,我测试了是可以拿到语音输入的原始内容的。
|
||||
|
||||
---
|
||||
|
||||
### 评论 10 - hanxi
|
||||
|
||||
文档更新了下,active_cmd 也需要配置一下才能正常唤醒。
|
||||
|
||||
---
|
||||
|
||||
### 评论 11 - CZJCC
|
||||
|
||||
> @CZJCC 你可以更新看看 plugins/code1.py 的测试代码,我测试了是可以拿到语音输入的原始内容的。
|
||||
|
||||
我原先设想的事用户的话术是”通义提问为什么地球是圆的“,指令匹配的时候通义提问前缀匹配到类似于code1方法,为什么地球是圆的作为参数传入这个函数,但我看现在是拿历史记录实现的
|
||||
|
||||
---
|
||||
|
||||
### 评论 12 - hanxi
|
||||
|
||||
是的,插件函数里面再切割一下前缀就行。last_record就是当前的那条语音数据。
|
||||
|
||||
---
|
||||
|
||||
### 评论 13 - hanxi
|
||||
|
||||
> > @CZJCC 你可以更新看看 plugins/code1.py 的测试代码,我测试了是可以拿到语音输入的原始内容的。
|
||||
>
|
||||
> 我原先设想的事用户的话术是”通义提问为什么地球是圆的“,指令匹配的时候通义提问前缀匹配到类似于code1方法,为什么地球是圆的作为参数传入这个函数,但我看现在是拿历史记录实现的
|
||||
|
||||
是的,这样比较简单,交给插件里面处理也比较自由。
|
||||
|
||||
---
|
||||
|
||||
### 评论 14 - mogeqian
|
||||
|
||||
key_word_dict中的“播放歌曲”口令是不能修改的是吧?因为以前用小爱同学播放歌曲说习惯了,总是触发xiaomusic自动下载歌曲,我想把口令改成“查找歌曲”,这样我当说播放歌曲的时候就调用网易云音乐或者QQ音乐,当我说查找歌曲的时候就先看本地有没有歌曲,没有话就自动下载到本地。
|
||||
当我先按照config-example.json的模板写好如下配置并重命名为config.json
|
||||
```
|
||||
"key_word_dict": {
|
||||
"查找歌曲": "play",
|
||||
"播放本地歌曲": "playlocal",
|
||||
"关机": "stop",
|
||||
"下一首": "play_next",
|
||||
"单曲循环": "set_play_type_one",
|
||||
"全部循环": "set_play_type_all",
|
||||
"随机播放": "set_random_play",
|
||||
"分钟后关机": "stop_after_minute",
|
||||
"播放列表": "play_music_list",
|
||||
"刷新列表": "gen_music_list",
|
||||
"本地播放歌曲": "playlocal",
|
||||
"下载歌曲": "play",
|
||||
"暂停": "stop",
|
||||
"停止": "stop",
|
||||
"停止播放": "stop",
|
||||
"测试自定义口令": "exec#code1(\"hello\")",
|
||||
"测试链接": "exec#httpget(\"https://github.com/hanxi/xiaomusic\")"
|
||||
},
|
||||
"key_match_order": [
|
||||
"分钟后关机",
|
||||
"查找歌曲",
|
||||
"下一首",
|
||||
"单曲循环",
|
||||
"全部循环",
|
||||
"随机播放",
|
||||
"关机",
|
||||
"刷新列表",
|
||||
"播放列表",
|
||||
"播放本地歌曲",
|
||||
"本地播放歌曲",
|
||||
"下载歌曲",
|
||||
"暂停",
|
||||
"停止",
|
||||
"停止播放",
|
||||
"测试自定义口令",
|
||||
"测试链接"
|
||||
],
|
||||
```
|
||||
|
||||
用以下命令安装docker
|
||||
`docker run --name xiaomusic -p 5488:5488 -v /mnt/sharedata/audiodata/musci/xiaomusic:/app/music -v /mnt/data_sdb1/docker/xiaomusic/config.json:/app/config.json -e XIAOMUSIC_PORT=5488 hanxi/xiaomusic --config /app/config.json`
|
||||
日志里提示的依然是:
|
||||
|
||||
` key_word_dict={'播放歌曲': 'play', '播放本地歌曲': 'playlocal', '关机': 'stop', '下一首': 'play_next', '上一首': 'play_prev', '单曲循环': 'set_play_type_one', '全部循环': 'set_play_type_all', '随机播放': 'set_random_play', '分钟后关机': 'stop_after_minute', '播放列表': 'play_music_list', '刷新列表': 'gen_music_list', '加入收藏': 'add_to_favorites', '收藏歌曲': 'add_to_favorites', '取消收藏': 'del_from_favorites', '播放列表第': 'play_music_list_index', '本地播放歌曲': 'playlocal', '查找歌曲': 'play', '下载歌曲': 'play', '暂停': 'stop', '停止': 'stop', '停止播放': 'stop', '播放歌单': 'play_music_list', '测试自定义口令': 'exec#code1("hello")', '测试链接': 'exec#httpget("https://github.com/hanxi/xiaomusic")'}, key_match_order=['分钟后关机', '播放歌曲', '下一首', '上一首', '单曲循环', '全部循环', '随机播放', '关机', '刷新列表', '播放列表第', '播放列表', '加入收藏', '收藏歌曲', '取消收藏', '播放本地歌曲', '本地播放歌曲', '查找歌曲', '下载歌曲', '暂停', '停止', '停止播放', '播放歌单', '测试自定义口令', '测试链接']`
|
||||
|
||||
似乎自定义的口令只能以插入的方式添加上去,并不能替换掉原来的口令
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 评论 15 - hanxi
|
||||
|
||||
> key_word_dict中的“播放歌曲”口令是不能修改的是吧?因为以前用小爱同学播放歌曲说习惯了,总是触发xiaomusic自动下载歌曲,我想把口令改成“查找歌曲”,这样我当说播放歌曲的时候就调用网易云音乐或者QQ音乐,当我说查找歌曲的时候就先看本地有没有歌曲,没有话就自动下载到本地。 当我先按照config-example.json的模板写好如下配置并重命名为config.json
|
||||
>
|
||||
> ```
|
||||
> "key_word_dict": {
|
||||
> "查找歌曲": "play",
|
||||
> "播放本地歌曲": "playlocal",
|
||||
> "关机": "stop",
|
||||
> "下一首": "play_next",
|
||||
> "单曲循环": "set_play_type_one",
|
||||
> "全部循环": "set_play_type_all",
|
||||
> "随机播放": "set_random_play",
|
||||
> "分钟后关机": "stop_after_minute",
|
||||
> "播放列表": "play_music_list",
|
||||
> "刷新列表": "gen_music_list",
|
||||
> "本地播放歌曲": "playlocal",
|
||||
> "下载歌曲": "play",
|
||||
> "暂停": "stop",
|
||||
> "停止": "stop",
|
||||
> "停止播放": "stop",
|
||||
> "测试自定义口令": "exec#code1(\"hello\")",
|
||||
> "测试链接": "exec#httpget(\"https://github.com/hanxi/xiaomusic\")"
|
||||
> },
|
||||
> "key_match_order": [
|
||||
> "分钟后关机",
|
||||
> "查找歌曲",
|
||||
> "下一首",
|
||||
> "单曲循环",
|
||||
> "全部循环",
|
||||
> "随机播放",
|
||||
> "关机",
|
||||
> "刷新列表",
|
||||
> "播放列表",
|
||||
> "播放本地歌曲",
|
||||
> "本地播放歌曲",
|
||||
> "下载歌曲",
|
||||
> "暂停",
|
||||
> "停止",
|
||||
> "停止播放",
|
||||
> "测试自定义口令",
|
||||
> "测试链接"
|
||||
> ],
|
||||
> ```
|
||||
>
|
||||
> 用以下命令安装docker `docker run --name xiaomusic -p 5488:5488 -v /mnt/sharedata/audiodata/musci/xiaomusic:/app/music -v /mnt/data_sdb1/docker/xiaomusic/config.json:/app/config.json -e XIAOMUSIC_PORT=5488 hanxi/xiaomusic --config /app/config.json` 日志里提示的依然是:
|
||||
>
|
||||
> ` key_word_dict={'播放歌曲': 'play', '播放本地歌曲': 'playlocal', '关机': 'stop', '下一首': 'play_next', '上一首': 'play_prev', '单曲循环': 'set_play_type_one', '全部循环': 'set_play_type_all', '随机播放': 'set_random_play', '分钟后关机': 'stop_after_minute', '播放列表': 'play_music_list', '刷新列表': 'gen_music_list', '加入收藏': 'add_to_favorites', '收藏歌曲': 'add_to_favorites', '取消收藏': 'del_from_favorites', '播放列表第': 'play_music_list_index', '本地播放歌曲': 'playlocal', '查找歌曲': 'play', '下载歌曲': 'play', '暂停': 'stop', '停止': 'stop', '停止播放': 'stop', '播放歌单': 'play_music_list', '测试自定义口令': 'exec#code1("hello")', '测试链接': 'exec#httpget("https://github.com/hanxi/xiaomusic")'}, key_match_order=['分钟后关机', '播放歌曲', '下一首', '上一首', '单曲循环', '全部循环', '随机播放', '关机', '刷新列表', '播放列表第', '播放列表', '加入收藏', '收藏歌曲', '取消收藏', '播放本地歌曲', '本地播放歌曲', '查找歌曲', '下载歌曲', '暂停', '停止', '停止播放', '播放歌单', '测试自定义口令', '测试链接']`
|
||||
>
|
||||
> 似乎自定义的口令只能以插入的方式添加上去,并不能替换掉原来的口令
|
||||
|
||||
可以在网页后台设置页面改。
|
||||
|
||||
---
|
||||
|
||||
### 评论 16 - mogeqian
|
||||
|
||||
不行,后台设置如图
|
||||

|
||||
日志如下:
|
||||
```
|
||||
[2024-11-11 18:08:04] [0.3.46] [INFO] xiaomusic.py:1130: update_config_from_setting ok. data:Config(account='**', password='**', mi_did='726577518,570867755', miio_tts_command='', cookie='', verbose=False, music_path='music', download_path='music/download', conf_path='conf', cache_dir='cache', hostname='192.168.22.4', port=8090, public_port=0, proxy='', search_prefix='bilisearch:', ffmpeg_location='./ffmpeg/bin', active_cmd='play,set_random_play,playlocal,play_music_list,stop', exclude_dirs='@eaDir', music_path_depth=10, disable_httpauth=True, httpauth_username='******', httpauth_password='******', music_list_url='', music_list_json='', custom_play_list_json='', disable_download=False, key_word_dict={'播放歌曲': 'play', '播放本地歌曲': 'playlocal', '关机': 'stop', '下一首': 'play_next', '上一首': 'play_prev', '单曲循环': 'set_play_type_one', '全部循环': 'set_play_type_all', '随机播放': 'set_random_play', '分钟后关机': 'stop_after_minute', '播放列表': 'play_music_list', '刷新列表': 'gen_music_list', '加入收藏': 'add_to_favorites', '收藏歌曲': 'add_to_favorites', '取消收藏': 'del_from_favorites', '播放列表第': 'play_music_list_index', '本地播放歌曲': 'playlocal', '查找歌曲': 'play', '下载歌曲': 'play', '暂停': 'stop', '停止': 'stop', '停止播放': 'stop', '播放歌单': 'play_music_list', '测试自定义口令': 'exec#code1("hello")', '测试链接': 'exec#httpget("https://github.com/hanxi/xiaomusic")'}, key_match_order=['分钟后关机', '播放歌曲', '下一首', '上一首', '单曲循环', '全部循环', '随机播放', '关机', '刷新列表', '播放列表第', '播放列表', '加入收藏', '收藏歌曲', '取消收藏', '播放本地歌曲', '本地播放歌曲', '查找歌曲', '下载歌曲', '暂停', '停止', '停止播放', '播放歌单', '测试自定义口令', '测试链接'], use_music_api=False, use_music_audio_id='1582971365183456177', use_music_id='355454500', log_file='/tmp/xiaomusic.txt', fuzzy_match_cutoff=0.6, enable_fuzzy_match=True, stop_tts_msg='收到,再见', enable_config_example=False, keywords_playlocal='播放本地歌曲,本地播放歌曲', keywords_play='查找歌曲,下载歌曲', keywords_stop='关机,暂停,停止,停止播放', keywords_playlist='播放列表,播放歌单', user_key_word_dict={'测试自定义口令': 'exec#code1("hello")', '测试链接': 'exec#httpget("https://github.com/hanxi/xiaomusic")'}, enable_force_stop=False, devices={'726577518': Device(did='726577518', device_id='******', hardware='LX06', name='小爱音箱Pro', play_type='', cur_music='', cur_playlist='全部'), '570867755': Device(did='570867755', device_id='*********', hardware='L15A', name='小米AI音箱(第二代)', play_type='', cur_music='', cur_playlist='全部')}, group_list='', remove_id3tag=False, convert_to_mp3=False, delay_sec=3, continue_play=False, pull_ask_sec=1, crontab_json='', enable_yt_dlp_cookies=False, get_ask_by_mina=False)
|
||||
[2024-11-11 18:08:04] [0.3.46] [INFO] xiaomusic.py:1133: 语音控制已启动, 用【分钟后关机/播放歌曲/下一首/上一首/单曲循环/全部循环/随机播放/关机/刷新列表/播放列表第/播放列表/加入收藏/收藏歌曲/取消收藏/播放本地歌曲/本地播放歌曲/查找歌曲/下载歌曲/暂停/停止/停止播放/播放歌单/测试自定义口令/测试链接】开头来控制
|
||||
[2024-11-11 18:08:04] [0.3.46] [INFO] xiaomusic.py:543: 协程时间循环未启动
|
||||
[2024-11-11 18:08:04] [0.3.46] [INFO] xiaomusic.py:1250: 没打乱 全部 ['乌兰托娅 火红的萨日朗', '凤凰传奇麝香夫人'] ... ['罗大佑童年', '阿嬷'] with len: 7
|
||||
[2024-11-11 18:08:04] [0.3.46] [INFO] xiaomusic.py:1250: 没打乱 全部 ['乌兰托娅 火红的萨日朗', '凤凰传奇麝香夫人'] ... ['罗大佑童年', '阿嬷'] with len: 7
|
||||
[2024-11-11 18:08:04] [0.3.46] [INFO] analytics.py:28: analytics init ok
|
||||
[2024-11-11 18:08:04] [0.3.46] [INFO] xiaomusic.py:104: Startup OK. Config(account='***', password='***', mi_did='726577518,570867755', miio_tts_command='', cookie='', verbose=False, music_path='music', download_path='music/download', conf_path='conf', cache_dir='cache', hostname='192.168.22.4', port=8090, public_port=0, proxy='', search_prefix='bilisearch:', ffmpeg_location='./ffmpeg/bin', active_cmd='play,set_random_play,playlocal,play_music_list,stop', exclude_dirs='@eaDir', music_path_depth=10, disable_httpauth=True, httpauth_username='******', httpauth_password='******', music_list_url='', music_list_json='', custom_play_list_json='', disable_download=False, key_word_dict={'播放歌曲': 'play', '播放本地歌曲': 'playlocal', '关机': 'stop', '下一首': 'play_next', '上一首': 'play_prev', '单曲循环': 'set_play_type_one', '全部循环': 'set_play_type_all', '随机播放': 'set_random_play', '分钟后关机': 'stop_after_minute', '播放列表': 'play_music_list', '刷新列表': 'gen_music_list', '加入收藏': 'add_to_favorites', '收藏歌曲': 'add_to_favorites', '取消收藏': 'del_from_favorites', '播放列表第': 'play_music_list_index', '本地播放歌曲': 'playlocal', '查找歌曲': 'play', '下载歌曲': 'play', '暂停': 'stop', '停止': 'stop', '停止播放': 'stop', '播放歌单': 'play_music_list', '测试自定义口令': 'exec#code1("hello")', '测试链接': 'exec#httpget("https://github.com/hanxi/xiaomusic")'}, key_match_order=['分钟后关机', '播放歌曲', '下一首', '上一首', '单曲循环', '全部循环', '随机播放', '关机', '刷新列表', '播放列表第', '播放列表', '加入收藏', '收藏歌曲', '取消收藏', '播放本地歌曲', '本地播放歌曲', '查找歌曲', '下载歌曲', '暂停', '停止', '停止播放', '播放歌单', '测试自定义口令', '测试链接'], use_music_api=False, use_music_audio_id='1582971365183456177', use_music_id='355454500', log_file='/tmp/xiaomusic.txt', fuzzy_match_cutoff=0.6, enable_fuzzy_match=True, stop_tts_msg='收到,再见', enable_config_example=False, keywords_playlocal='播放本地歌曲,本地播放歌曲', keywords_play='查找歌曲,下载歌曲', keywords_stop='关机,暂停,停止,停止播放', keywords_playlist='播放列表,播放歌单', user_key_word_dict={'测试自定义口令': 'exec#code1("hello")', '测试链接': 'exec#httpget("https://github.com/hanxi/xiaomusic")'}, enable_force_stop=False, devices={'726577518': Device(did='726577518', device_id='*****', hardware='LX06', name='小爱音箱Pro', play_type='', cur_music='', cur_playlist='全部'), '570867755': Device(did='570867755', device_id='*********', hardware='L15A', name='小米AI音箱(第二代)', play_type='', cur_music='', cur_playlist='全部')}, group_list='', remove_id3tag=False, convert_to_mp3=False, delay_sec=3, continue_play=False, pull_ask_sec=1, crontab_json='', enable_yt_dlp_cookies=False, get_ask_by_mina=False)
|
||||
[2024-11-11 18:08:04] [0.3.46] [INFO] httpserver.py:111: disable_httpauth:True
|
||||
[18:08:04] [0.3.46] [INFO] Started server process [1]
|
||||
[18:08:04] [0.3.46] [INFO] Waiting for application startup.
|
||||
[2024-11-11 18:08:04] [0.3.46] [INFO] xiaomusic.py:541: 启动后台构建 tag cache
|
||||
[18:08:04] [0.3.46] [INFO] Application startup complete.
|
||||
[2024-11-11 18:08:04] [0.3.46] [INFO] xiaomusic.py:513: 已从【cache/tag_cache.json】加载 tag cache
|
||||
[18:08:04] [0.3.46] [INFO] Uvicorn running on http://['0.0.0.0', '::']:8090 (Press CTRL+C to quit)
|
||||
[2024-11-11 18:08:04] [0.3.46] [INFO] xiaomusic.py:527: 保存:tag cache 已保存到【cache/tag_cache.json】
|
||||
[2024-11-11 18:08:04] [0.3.46] [INFO] xiaomusic.py:577: tag 更新完成
|
||||
[2024-11-11 18:08:04] [0.3.46] [INFO] xiaomusic.py:248: 选中的设备: {'726577518': Device(did='726577518', device_id='******', hardware='LX06', name='小爱音箱Pro', play_type='', cur_music='', cur_playlist='全部'), '570867755': Device(did='570867755', device_id='*********', hardware='L15A', name='小米AI音箱(第二代)', play_type='', cur_music='', cur_playlist='全部')}
|
||||
[18:08:34] [0.3.46] [INFO] 172.20.0.1:35058 - "GET /static/default/setting.html HTTP/1.1" 304
|
||||
[18:08:34] [0.3.46] [INFO] 172.20.0.1:35058 - "GET /getversion HTTP/1.1" 200
|
||||
```
|
||||
使用的docker-compose命令安装
|
||||
services:
|
||||
xiaomusic:
|
||||
image: hanxi/xiaomusic
|
||||
container_name: xiaomusic
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 8090:8090
|
||||
volumes:
|
||||
- /mnt/sharedata/audiodata/musci/xiaomusic:/app/music
|
||||
- /mnt/data_sdb1/docker/xiaomusic/config.json:/app/config.json
|
||||
command: ['--config', '/app/config.json']
|
||||
|
||||
根据日志的提示,'播放歌曲': 'play'依然存在,只是增加了 '查找歌曲': 'play', '下载歌曲': 'play', 这两个关于play的自定义口令,所以实际上play有三条口令 “播放歌曲、查找歌曲、下载歌曲”,能否删除掉'播放歌曲': 'play'这个系统默认的口令?只使用 '查找歌曲': 'play', '下载歌曲': 'play', 这两个关于play的自定义口令
|
||||
|
||||
---
|
||||
|
||||
### 评论 17 - hanxi
|
||||
|
||||
@mogeqian 另外提个 issue 吧,现在应该是不支持删除默认的口令。
|
||||
|
||||
---
|
||||
|
||||
### 评论 18 - mogeqian
|
||||
|
||||
好的,已经重开了一个issue #259
|
||||
|
||||
---
|
||||
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/105)
|
||||
111
docs/issues/182.md
Normal file
111
docs/issues/182.md
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
title: 定时任务配置格式
|
||||
---
|
||||
|
||||
# 定时任务配置格式
|
||||
|
||||
支持采用 crontab 的格式配置定时任务,已经支持下面的任务类型:
|
||||
|
||||
- stop 关机
|
||||
- play 播放歌曲
|
||||
- play_music_list 播放列表
|
||||
- tts 文字转语音
|
||||
- refresh_music_list 刷新播放列表
|
||||
- set_volume 设置音量
|
||||
- set_play_type 设置播放类型,单曲循环 0 , 全部循环 1 , 随机播放 2 , 单曲播放 3 , 顺序播放 4
|
||||
|
||||
### 示例
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"expression": "0 8 * * 0-4",
|
||||
"name": "play",
|
||||
"did": "123456789",
|
||||
"arg1": "周杰伦晴天"
|
||||
},
|
||||
{
|
||||
"expression": "10 8 * * 0-4",
|
||||
"name": "stop",
|
||||
"did": "123456789"
|
||||
},
|
||||
{
|
||||
"expression": "0 9 * * *",
|
||||
"name": "play",
|
||||
"did": "123456789",
|
||||
"arg1": "周杰伦晴天"
|
||||
},
|
||||
{
|
||||
"expression": "0 10 * * *",
|
||||
"name": "play_music_list",
|
||||
"did": "123456789",
|
||||
"arg1": "周杰伦"
|
||||
},
|
||||
{
|
||||
"expression": "30 10 * * *",
|
||||
"name": "play_music_list",
|
||||
"did": "123456789",
|
||||
"arg1": "周杰伦|晴天"
|
||||
},
|
||||
{
|
||||
"expression": "0 7 * * *",
|
||||
"name": "tts",
|
||||
"did": "123456789",
|
||||
"arg1": "早上好!该起床了!"
|
||||
},
|
||||
{
|
||||
"expression": "0 3 * * *",
|
||||
"name": "refresh_music_list"
|
||||
},
|
||||
{
|
||||
"expression": "* * * * *",
|
||||
"name": "set_volume",
|
||||
"did": "123456789",
|
||||
"arg1": "25"
|
||||
},
|
||||
{
|
||||
"expression": "* * * * *",
|
||||
"name": "set_play_type",
|
||||
"did": "123456789",
|
||||
"arg1": "2"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
示例中的意思是:
|
||||
|
||||
- 周一到周五每天 8 点播放歌曲 "周杰伦晴天"
|
||||
- 周一到周五每天 8 点 10 分执行关机指令
|
||||
- 每天 9 点播放歌曲 "周杰伦晴天"
|
||||
- 每天 10 点播放列表 "周杰伦"
|
||||
- 每天 10 点 30 分播放列表 "周杰伦" 里的 "晴天"
|
||||
- 每天 7 点发出语音 "早上好!该起床了!"
|
||||
- 每天 3 点刷新播放列表,用于自动更新目录下的歌曲到播放列表里。
|
||||
- 每分钟设置音量为 25
|
||||
- 每分钟设置为随机播放
|
||||
|
||||
> 注意星期一是0,星期二是1,星期日是6。
|
||||
> (0-6 or mon,tue,wed,thu,fri,sat,sun)
|
||||
> The first weekday is always monday.
|
||||
|
||||
### 参数意思
|
||||
|
||||
- expression 的格式是标准的 crontab 的格式,用于配置任务的执行时机,如何配置可以直接用下面的 crontab ai 工具生成
|
||||
- <https://cronly.app/ai>
|
||||
- <https://cronify.zimo.li/>
|
||||
- name 是任务名,目前只支持上面那几种。
|
||||
- did 是小爱音箱的设备ID,就是设置页面的音箱型号后面的那串数字。
|
||||
- arg1 根据任务不同而不同。
|
||||
- play 的 arg1 表示要播放的歌曲名。
|
||||
- play_music_list 的 arg1 表示要播放的播放目录名,可以加 `|` 符合加上目录下面的歌曲名,也可不加。
|
||||
- tts 的 arg1 表示要说的语音文字。
|
||||
|
||||
## 评论
|
||||
|
||||
|
||||
### 评论 1 - hanxi
|
||||
|
||||
0.3.38版本功能。
|
||||
|
||||
---
|
||||
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/182)
|
||||
130
docs/issues/19.md
Normal file
130
docs/issues/19.md
Normal file
@@ -0,0 +1,130 @@
|
||||
---
|
||||
title: 如何修改默认的8090端口
|
||||
---
|
||||
|
||||
# 如何修改默认的8090端口
|
||||
|
||||
docker-compose 修改映射端口会播放失败
|
||||
|
||||
```
|
||||
ports:
|
||||
- 80:8090
|
||||
```
|
||||
|
||||
从日志看继续调用了 http://10.0.0.4:8090 而不是修改映射的80,还原成
|
||||
|
||||
```
|
||||
ports:
|
||||
- 8090:8090
|
||||
```
|
||||
则一切正常
|
||||
|
||||
```
|
||||
xiaomusic | [BiliBiliSearch] Playlist 安河桥北: Downloading 1 items of 1
|
||||
xiaomusic | [download] Downloading item 1 of 1
|
||||
xiaomusic | [BiliBili] Extracting URL: http://www.bilibili.com/video/av319943893
|
||||
xiaomusic | [BiliBili] 319943893: Downloading webpage
|
||||
xiaomusic | 10.0.0.118 - - [21/Feb/2024 15:29:24] "POST /cmd HTTP/1.1" 200 -
|
||||
xiaomusic | 10.0.0.118 - - [21/Feb/2024 15:29:24] "POST /cmd HTTP/1.1" 200 -
|
||||
xiaomusic | 10.0.0.118 - - [21/Feb/2024 15:29:24] "POST /cmd HTTP/1.1" 200 -
|
||||
xiaomusic | 10.0.0.118 - - [21/Feb/2024 15:29:24] "POST /cmd HTTP/1.1" 200 -
|
||||
xiaomusic | [BiliBili] BV1tw411X7Rr: Extracting videos in anthology
|
||||
xiaomusic | 10.0.0.118 - - [21/Feb/2024 15:29:24] "POST /cmd HTTP/1.1" 200 -
|
||||
xiaomusic | [BiliBili] 319943893: Extracting chapters
|
||||
xiaomusic | 10.0.0.118 - - [21/Feb/2024 15:29:24] "POST /cmd HTTP/1.1" 200 -
|
||||
xiaomusic | 10.0.0.118 - - [21/Feb/2024 15:29:24] "POST /cmd HTTP/1.1" 200 -
|
||||
xiaomusic | [BiliBili] Format(s) 1080P 高码率, 1080P 高清, 720P 高清, 4K 超清 are missing; you have to login or become premium member to download them. Use --cookies-from-browser or --cookies for the authentication. See https://github.com/yt-dlp/yt-dlp/wiki/FAQ#how-do-i-pass-cookies-to-yt-dlp for how to manually pass cookies
|
||||
xiaomusic | [info] BV1tw411X7Rr: Downloading 1 format(s): 30280
|
||||
xiaomusic | 10.0.0.118 - - [21/Feb/2024 15:29:24] "POST /cmd HTTP/1.1" 200 -
|
||||
xiaomusic | 10.0.0.118 - - [21/Feb/2024 15:29:24] "POST /cmd HTTP/1.1" 200 -
|
||||
xiaomusic | [download] Destination: music/安河桥北.m4a
|
||||
[download] 0.3% of 5.61MiB at 6.24MiB/s ETA 00:0010.0.0.118 - - [21/Feb/2024 15:29:24] "POST /cmd HTTP/1.1" 200 -
|
||||
[download] 0.5% of 5.61MiB at 2.42MiB/s ETA 00:0210.0.0.118 - - [21/Feb/2024 15:29:24] "POST /cmd HTTP/1.1" 200 -
|
||||
[download] 1.1% of 5.61MiB at 1.50MiB/s ETA 00:0310.0.0.118 - - [21/Feb/2024 15:29:24] "POST /cmd HTTP/1.1" 200 -
|
||||
[download] 4.4% of 5.61MiB at 2.35MiB/s ETA 00:0210.0.0.118 - - [21/Feb/2024 15:29:24] "POST /cmd HTTP/1.1" 200 -
|
||||
[download] 8.9% of 5.61MiB at 3.59MiB/s ETA 00:0110.0.0.118 - - [21/Feb/2024 15:29:24] "POST /cmd HTTP/1.1" 200 -
|
||||
[download] 100% of 5.61MiB in 00:00:00 at 10.83MiB/s
|
||||
xiaomusic | [ExtractAudio] Destination: music/安河桥北.mp3
|
||||
xiaomusic | Deleting original file music/安河桥北.m4a (pass -k to keep)
|
||||
xiaomusic | [download] Finished downloading playlist: 安河桥北
|
||||
xiaomusic | [02/21/24 15:29:29] INFO 播放 xiaomusic.py:461
|
||||
xiaomusic | http://10.0.0.4:8090/music/%E5%AE%
|
||||
xiaomusic | 89%E6%B2%B3%E6%A1%A5%E5%8C%97.mp3
|
||||
xiaomusic | INFO 已经开始播放了 xiaomusic.py:464
|
||||
xiaomusic | INFO 歌曲music/安河桥北.mp3的时长251秒 xiaomusic.py:371
|
||||
xiaomusic | INFO 251秒后将会播放下一首 xiaomusic.py:385
|
||||
xiaomusic | INFO 匹配到指令. opkey:set_volume# xiaomusic.py:441
|
||||
xiaomusic | opvalue:set_volume oparg:24
|
||||
```
|
||||
|
||||
## 评论
|
||||
|
||||
|
||||
### 评论 1 - hanxi
|
||||
|
||||
需要添加环境变量
|
||||
```
|
||||
environment:
|
||||
XIAOMUSIC_PORT:80
|
||||
ports:
|
||||
- 80:80
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 评论 2 - newrookie001
|
||||
|
||||
> 需要添加环境变量
|
||||
>
|
||||
> ```
|
||||
> environment:
|
||||
> XIAOMUSIC_PORT:80
|
||||
> ports:
|
||||
> - 80:80
|
||||
> ```
|
||||
|
||||
自己走了点弯路,半天才搞明白。补充说明:
|
||||
> environment:
|
||||
> XIAOMUSIC_PORT: 5678 #就是“5678”可以根据自己要求设置,但要求上下的5678都设置成一个
|
||||
> ports:
|
||||
> - 5678:5678
|
||||
|
||||
---
|
||||
|
||||
### 评论 3 - hanxi
|
||||
|
||||
如果换端口,需要3个数字一致,比如
|
||||
|
||||
```
|
||||
environment:
|
||||
XIAOMUSIC_PORT:6874
|
||||
ports:
|
||||
- 6874:6874
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 评论 4 - hanxi
|
||||
|
||||
文档类型的我都打开下,方便其他人看到。
|
||||
|
||||
---
|
||||
|
||||
### 评论 5 - flymin
|
||||
|
||||
docker-compose 中对应关系应该是
|
||||
```yaml
|
||||
ports:
|
||||
- aaaa:bbbb
|
||||
environment:
|
||||
XIAOMUSIC_PORT: bbbb # 配置文件中的 port,后台:监听端口(修改后需要重启)
|
||||
XIAOMUSIC_PUBLIC_PORT: aaaa # 配置文件中的 public_port,后台:外网访问端口(0表示跟监听端口一致)
|
||||
```
|
||||
|
||||
以上,docker 环境中基本不存在需要修改 bbbb 的情况,也就是不用设置 XIAOMUSIC_PORT。如果需要修改端口,只需要修改两处 aaaa
|
||||
如果使用反向代理,则转发 localhost:aaaa,XIAOMUSIC_PUBLIC_PORT 设置成代理的监听端口 cccc
|
||||
|
||||
另外,setting 文件存在会覆盖环境变量。启动过之后需要直接修改 settings.json 或者在后台修改
|
||||
|
||||
---
|
||||
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/19)
|
||||
73
docs/issues/210.md
Normal file
73
docs/issues/210.md
Normal file
@@ -0,0 +1,73 @@
|
||||
---
|
||||
title: yt-dlp cookies 文件上传功能
|
||||
---
|
||||
|
||||
# yt-dlp cookies 文件上传功能
|
||||
|
||||
此功能用于解决 yt-dlp 下载资源失败时使用,比如 ip 被 B站或者 youtube 加入黑名单后才需要使用。
|
||||
|
||||
上传的文件用于 yt-dlp 的 `--cookies` 参数。
|
||||
```
|
||||
--cookies FILE Netscape formatted file to read cookies from
|
||||
and dump cookie jar in
|
||||
```
|
||||
|
||||
## 获取 cookies.txt 文件
|
||||
|
||||
1. 下载插件 [Get cookies.txt LOCALLY](https://chromewebstore.google.com/detail/cclelndahbckbenkjhflpdbgdldlbecc)
|
||||
2. 给予插件访问权限和无痕模式允许使用
|
||||

|
||||
3. 打开无痕窗口
|
||||
4. 打开 youtube.com
|
||||
5. 登陆 youtube.com
|
||||
6. 打开新标签页
|
||||
7. 关闭 youtube.com 的标签页
|
||||
8. 保存 cookies.txt
|
||||

|
||||
|
||||
原因见 https://github.com/yt-dlp/yt-dlp/wiki/Extractors#exporting-youtube-cookies
|
||||
|
||||
## 上传 cookies.txt
|
||||
|
||||
1. 打开设置页面
|
||||
2. 设置启用yt-dlp-cookies 选项为 true
|
||||

|
||||
3. 点击保存
|
||||
4. 再点击选择文件,选择前面保存好的 cookies.txt 文件,点击上传。
|
||||

|
||||
|
||||
|
||||
## 后续用途
|
||||
|
||||
1. 语音下载歌曲都会带上前面上传的 cookies.txt 文件去搜索下载歌曲。
|
||||
2. 歌曲批量下载工具下载歌曲或者歌单时也会带上 cookies.txt 文件。
|
||||
|
||||
|
||||
|
||||
## 评论
|
||||
|
||||
|
||||
### 评论 1 - kingfly2016
|
||||
|
||||
0.3.37的版本并没有发现可以开启yt-dlp-cookies 并上传cookies文件的地方,尝试把导出的cookies.txt手工上传到conf目录下,没有生效.
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### 评论 2 - hanxi
|
||||
|
||||
需要等38版本,或者用测试版本,镜像名后面加 :main
|
||||
|
||||
---
|
||||
|
||||
### 评论 3 - kingfly2016
|
||||
|
||||
|
||||
|
||||
> 需要等38版本,或者用测试版本,镜像名后面加 :main
|
||||
|
||||
谢谢
|
||||
|
||||
---
|
||||
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/210)
|
||||
112
docs/issues/211.md
Normal file
112
docs/issues/211.md
Normal file
@@ -0,0 +1,112 @@
|
||||
---
|
||||
title: 📝 文档汇总
|
||||
---
|
||||
|
||||
# 📝 文档汇总
|
||||
|
||||
## 1️⃣ 基础文档
|
||||
|
||||
- [💬 FAQ问题集合](/issues/99.html)
|
||||
- [如何修改默认的8090端口](/issues/19.html)
|
||||
- [如何配置网络歌单](/issues/78.html)
|
||||
- [如何添加m3u格式文件的电台](/issues/88.html)
|
||||
- [xiaomusic极空间安装教程](/issues/297.html)
|
||||
|
||||
## 2️⃣ 进阶文档
|
||||
|
||||
- [设置项功能介绍](/issues/333.html)
|
||||
- [采用config.json配置方式](/issues/94.html)
|
||||
- [ios系统上的捷径配置](/issues/96.html)
|
||||
- [【插件】自定义口令功能](/issues/105.html)
|
||||
- [定时任务配置格式](/issues/182.html)
|
||||
- [yt-dlp cookies 文件上传功能](/issues/210.html)
|
||||
- [如何批量下载歌曲](/issues/212.html)
|
||||
- [设备分组播放](/issues/65.html#issuecomment-2215736529)
|
||||
- [如何播放小雅alist里的歌曲](/issues/128.html#issuecomment-2232867180)
|
||||
- [如何添加 网易云音乐playlist](/issues/269.html)
|
||||
- [相关工具推荐](/issues/285.html)
|
||||
|
||||
## 3️⃣ 其他安装文档
|
||||
|
||||
> [!NOTE]
|
||||
> 下面教程可能比较旧,只供参考
|
||||
|
||||
- [群晖docker安装 xiaomusic](/issues/101.html)
|
||||
- [NAS部署教程](https://post.m.smzdm.com/p/avpe7n99/)
|
||||
- [群晖部署教程](https://post.m.smzdm.com/p/a7px7dol/)
|
||||
- [QNAS部署教程](https://post.smzdm.com/p/a5xz5x63/)
|
||||
- [视频教程-群晖1](https://www.bilibili.com/video/BV1ZZpweHEtT/)
|
||||
- [视频教程-群晖2](https://www.bilibili.com/video/BV1JXxXeBEdY/)
|
||||
- [视频教程-拾光坞N3](https://www.bilibili.com/video/BV1q629YMEG6/)
|
||||
- [TechHive](https://mp.weixin.qq.com/s/4a41muFtPaFKtHeZYu795w)
|
||||
- [弹个AI](https://mp.weixin.qq.com/s/sIsKxB7Y8b83AhnvaWiMog)
|
||||
- [简单免费!教你用绿联NAS联动小爱音箱,私人音乐库也能语音点播](https://post.m.smzdm.com/p/a8pldgg7/)
|
||||
- [飞牛教程](https://mp.weixin.qq.com/s?t=pages/image_detail&__biz=MzkxODc1NDMwOA==&mid=2247483725&idx=1&sn=2d615f14733b9bf989557fa766b4e1fc)
|
||||
|
||||
|
||||
## 评论
|
||||
|
||||
|
||||
### 评论 1 - sghuenn
|
||||
|
||||
redmi小爱触屏音箱8,仍然需要打开“型号兼容模式”才能播放
|
||||
打开兼容模式后的问题有:每首歌曲播放完毕后都要再从头播放4、5秒才播放下一曲;语音命令小爱同学播放下一曲,它会从头开始播放当前歌曲。
|
||||
|
||||
---
|
||||
|
||||
### 评论 2 - hanxi
|
||||
|
||||
> redmi小爱触屏音箱8,仍然需要打开“型号兼容模式”才能播放 打开兼容模式后的问题有:每首歌曲播放完毕后都要再从头播放4、5秒才播放下一曲;语音命令小爱同学播放下一曲,它会从头开始播放当前歌曲。
|
||||
|
||||
播放歌曲的接口应该是有点问题,等有设备有开发能力的人来搞吧。
|
||||
|
||||
---
|
||||
|
||||
### 评论 3 - zhoukk37
|
||||
|
||||
想问下如何利用反向代理,完成使得小爱外网访问nas呢,您能提供一下关键词,我自己去检索学下一下吗
|
||||
|
||||
---
|
||||
|
||||
### 评论 4 - hanxi
|
||||
|
||||
> 想问下如何利用反向代理,完成使得小爱外网访问nas呢,您能提供一下关键词,我自己去检索学下一下吗
|
||||
|
||||
内网穿透,frp能实现,就是把局域网的端口映射成公网的端口。
|
||||
|
||||
---
|
||||
|
||||
### 评论 5 - Justlook99
|
||||
|
||||
按照飞牛的教程,部署成功了,一直没有设备显示出来,然后我也按照相应的问题集去处理:关闭本地代理。
|
||||
如果是nas运行的,网络由bridge改为host。
|
||||
米家app重新登陆。
|
||||
mi.com官网重新登陆。
|
||||
但是还是没有办法显示设备出来,请问到底是什么原因?最新的37版本。
|
||||
|
||||
---
|
||||
|
||||
### 评论 6 - hanxi
|
||||
|
||||
> 按照飞牛的教程,部署成功了,一直没有设备显示出来,然后我也按照相应的问题集去处理:关闭本地代理。 如果是nas运行的,网络由bridge改为host。 米家app重新登陆。 mi.com官网重新登陆。 但是还是没有办法显示设备出来,请问到底是什么原因?最新的37版本。
|
||||
|
||||
目前反馈的都是飞牛的用户,可能是飞牛有问题。
|
||||
|
||||
---
|
||||
|
||||
### 评论 7 - 3794313569
|
||||
|
||||
在同一个容器内,前后分别启动了mi-gpt和xiaomusic两个应用,现在通过日志发现,mi-gpt的日志一直在记录,语音需求基本都在mi-gpt这个应用响应了,请问下按照您现在设计的框架内,有没有办法可以实现这两个应用同时生效,或者稍后类似应用会有专用的通讯协议,保证多项应用在同一台机器上的响应。
|
||||
类似:语音命令-“播放本地歌曲”触发xiaomusic,“召唤”(mi-gpt配置的唤醒词)触发mi-gpt,等等。。。。。。
|
||||
暂时的办法就是买两个小爱音箱,不同的命名,然后一个应用配置一个did。
|
||||
|
||||
---
|
||||
|
||||
### 评论 8 - hanxi
|
||||
|
||||
> 在同一个容器内,前后分别启动了mi-gpt和xiaomusic两个应用,现在通过日志发现,mi-gpt的日志一直在记录,语音需求基本都在mi-gpt这个应用响应了,请问下按照您现在设计的框架内,有没有办法可以实现这两个应用同时生效,或者稍后类似应用会有专用的通讯协议,保证多项应用在同一台机器上的响应。 类似:语音命令-“播放本地歌曲”触发xiaomusic,“召唤”(mi-gpt配置的唤醒词)触发mi-gpt,等等。。。。。。 暂时的办法就是买两个小爱音箱,不同的命名,然后一个应用配置一个did。
|
||||
|
||||
可以分别部署到两个不同的容器里,两个应用的唤醒词是不同的,不会互相干扰。
|
||||
|
||||
---
|
||||
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/211)
|
||||
51
docs/issues/212.md
Normal file
51
docs/issues/212.md
Normal file
@@ -0,0 +1,51 @@
|
||||
---
|
||||
title: 如何批量下载歌曲
|
||||
---
|
||||
|
||||
# 如何批量下载歌曲
|
||||
|
||||
批量下载歌曲依赖的是 yt-dlp 批量下载播放列表里的视频并转为 mp3 实现的。
|
||||
|
||||
先进入到歌曲下载工具页面:
|
||||
|
||||
> 默认主题 => 设置 => 歌曲下载工具
|
||||
|
||||

|
||||
|
||||
|
||||
已经测试过 B 站和 youtube 两种播放列表,播放列表的链接是有要求,不能有其他多余参数。
|
||||
|
||||
比如 B 站的是这样的
|
||||
|
||||
https://m.bilibili.com/video/BV1WUsDezE88
|
||||
|
||||
youtube 的是这样的
|
||||
|
||||
https://m.youtube.com/playlist?list=PLUD2d-pqyvT6_ztf31hx-5SsUUvY5UsQn
|
||||
|
||||
输入歌单名字是用于保存的文件夹名字,最好不是已经存在的名字,每次下载歌单都取个新名字比较合适。
|
||||
|
||||
已知 youtube 需要上传无痕模式下的 cookies.txt 文件才能正常下载。具体步骤见 /issues/210.html 。
|
||||
|
||||
|
||||
也支持单独下载一个链接只有一首歌曲的。
|
||||
|
||||
## 评论
|
||||
|
||||
|
||||
### 评论 1 - lazybabyz
|
||||
|
||||
默认主题 => 设置 =>没有显示找到 歌曲下载工具,
|
||||
有一个 歌单地址 歌单内容: 输入B站测试地址显示返回无效
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### 评论 2 - hanxi
|
||||
|
||||
等0.3.38版本。
|
||||
|
||||
---
|
||||
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/212)
|
||||
23
docs/issues/235.md
Normal file
23
docs/issues/235.md
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
title: xiaomusic立体声
|
||||
---
|
||||
|
||||
# xiaomusic立体声
|
||||
|
||||
有多个不同版本的小爱,怎么能选择多个音箱一起播放?
|
||||
|
||||
## 评论
|
||||
|
||||
|
||||
### 评论 1 - hanxi
|
||||
|
||||
参考这个文档,配到一个组里就能同时播放,但是会有播放进度不一致的情况。 /issues/65.html#issuecomment-2215736529
|
||||
|
||||
---
|
||||
|
||||
### 评论 2 - F-loat
|
||||
|
||||
我这边先用一个音箱播放,然后米家里设置全屋播放,就能多个音箱同时播了,进度也同步,而且后续会自动全屋播放
|
||||
|
||||
---
|
||||
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/235)
|
||||
532
docs/issues/269.md
Normal file
532
docs/issues/269.md
Normal file
@@ -0,0 +1,532 @@
|
||||
---
|
||||
title: 如何添加 网易云音乐playlist
|
||||
---
|
||||
|
||||
# 如何添加 网易云音乐playlist
|
||||
|
||||
利用 NeteaseCloudMusicApi 获取歌单和播放地址
|
||||
我在你基础上改了一下,但是我的逻辑不合理
|
||||
https://github.com/dissipator/xiaomusic
|
||||
|
||||
## 评论
|
||||
|
||||
|
||||
### 评论 1 - hanxi
|
||||
|
||||
建议通过插件实现或者新增一个页面工具把歌单导出 json 。
|
||||
歌单的 json 格式见 /issues/78.html
|
||||
|
||||
---
|
||||
|
||||
### 评论 2 - qiujie8092916
|
||||
|
||||
> 利用 NeteaseCloudMusicApi 获取歌单和播放地址 我在你基础上改了一下,但是我的逻辑不合理 [dissipator/xiaomusic](https://github.com/dissipator/xiaomusic)
|
||||
|
||||
老哥在实现插件了吗?我也急需播放歌单
|
||||
我诉求是:我用网易云音乐单独新建了一个歌单,我往里面扔歌曲,以更新歌单。希望创建一个自定义的语音命令,让小爱同学随机播放这个歌单里的音乐。然后我通过在米家里执行,比如触发了「我回来了」的智能场景时,就让小爱音箱执行这个自定义的语音命令,就会自动播放我新建的这个歌单里的音乐了。
|
||||
现在的小爱音箱虽然能勉强实现,但是很垃圾。随机播放的随机性有问题,并且只能选择歌单里的 30 首歌。
|
||||
(老哥如果没实现的话,我可以尝试搞搞)
|
||||
|
||||
---
|
||||
|
||||
### 评论 3 - dissipator
|
||||
|
||||
我都能直接利用这个直接播放NeteaseCloudMusicApi这上的歌了。在配合unblk,无敌。我就是没设备,还在路上
|
||||
通过插件实现,非常好,就是不知道怎么开发,有文档我可以尝试一下。
|
||||
|
||||
---
|
||||
|
||||
### 评论 4 - dissipator
|
||||
|
||||
> 建议通过插件实现或者新增一个页面工具把歌单导出 json 。 歌单的 json 格式见 #78
|
||||
|
||||
|
||||
配置处直接填了api的接口
|
||||
http://127.0.0.1:3000/playlist/detail?id=12758992226
|
||||
我是直接再你歌单保存上强改的。
|
||||
|
||||
```python
|
||||
async def downloadjson(data: UrlInfo, Verifcation=Depends(verification)):
|
||||
log.info(data)
|
||||
url = data.url
|
||||
content = "[{"
|
||||
host = f"{url.split('/')[0]}//{url.split('/')[2]}"
|
||||
try:
|
||||
ret = "OK"
|
||||
jsons = await downloadfile(url,"json")
|
||||
# print(jsons)
|
||||
list_name = jsons['playlist']['name']
|
||||
content += '"name":"'+list_name+'","musics":['
|
||||
for song in jsons['playlist']['tracks']:
|
||||
content += f"{{\"name\":\"{song['name']}\",\"url\": \"{host}/song/url?br=999000&proxy=http:%2F%2F127.0.0.1:8080&realIP=211.161.244.70&id={song['id']}\"}}"
|
||||
except Exception as e:
|
||||
log.exception(f"Execption {e}")
|
||||
ret = "Download JSON file failed."
|
||||
content = content[:-1] + "]}]"
|
||||
```
|
||||
照着你的说明。而且能成功播发
|
||||
|
||||
```python
|
||||
@app.get("/musicinfo")
|
||||
async def musicinfo(
|
||||
name: str, musictag: bool = False, Verifcation=Depends(verification)
|
||||
):
|
||||
url = xiaomusic.get_music_url(name)
|
||||
if("song/url" in url):
|
||||
jsons = await downloadfile(url,"json")
|
||||
url = jsons['data'][0]['url']
|
||||
```
|
||||
播放处加了一个判断
|
||||
|
||||
---
|
||||
|
||||
### 评论 5 - hanxi
|
||||
|
||||
> > 建议通过插件实现或者新增一个页面工具把歌单导出 json 。 歌单的 json 格式见 #78
|
||||
|
||||
你的修改我看了,不太通用。生成json,再用现有的接口提交json更通用。
|
||||
|
||||
---
|
||||
|
||||
### 评论 6 - dissipator
|
||||
|
||||
是的,不通用。最好是用插件实现。
|
||||
1. 就是不知道你插件的逻辑。
|
||||
2. 如果用插件就考虑直接读取网易账号下所有歌单。然后选择一个导入,或者全部导入。
|
||||
3. 等你完善文档后我可以尝试写一个。同理,qq等其他平台的歌单也就都可以弄了
|
||||
|
||||
---
|
||||
|
||||
### 评论 7 - hanxi
|
||||
|
||||
等有空我写个修改歌单内容的插件示例吧。
|
||||
|
||||
---
|
||||
|
||||
### 评论 8 - dissipator
|
||||
|
||||
```
|
||||
import requests
|
||||
|
||||
def getmy_playlist(type="netease",api_host="http://127.0.0.1/api", playlist_id=None,uid=None):
|
||||
"""
|
||||
Purpose:
|
||||
"""
|
||||
global log, xiaomusic
|
||||
if type == "netease":
|
||||
if uid:
|
||||
api_url = f"{api_host}/user/playlist?uid={uid}"
|
||||
# 发起请求
|
||||
response = requests.get(api_url, timeout=5) # 增加超时以避免长时间挂起
|
||||
response.raise_for_status() # 如果响应不是200,引发HTTPError异常
|
||||
# log.info(f"getmy_playlist url:{api_url} response:{response.text}")
|
||||
|
||||
music_list = response.json()
|
||||
for item in music_list['playlist']:
|
||||
list_name = item.get("name")
|
||||
|
||||
log.info(f"getmy_playlist name:{list_name}")
|
||||
# if item.get("id") in [12709941656,]:
|
||||
songs_url = f"{api_host}/playlist/detail?id={item['id']}"
|
||||
# 发起请求
|
||||
response = requests.get(songs_url, timeout=5) # 增加超时以避免长时间挂起
|
||||
response.raise_for_status() # 如果响应不是200,引发HTTPError异常
|
||||
# log.info(f"getmy_playlist url:{api_url} response:{response.text}")
|
||||
musics = response.json()
|
||||
one_music_list = []
|
||||
for music in musics['playlist']['tracks']:
|
||||
if (not music):
|
||||
continue
|
||||
# try:
|
||||
name = music['name']
|
||||
picUrl = music['al']['picUrl']
|
||||
artist = music['ar'][0]['name']
|
||||
album = music['al']['name']
|
||||
|
||||
name = music.get("name")
|
||||
url = f"{api_host}/song/url?id={music['id']}&br=350000&realIP=211.161.244.70&proxy=HTTP:%2F%2F127.0.0.1:8080"
|
||||
if (not name) or (not url):
|
||||
continue
|
||||
xiaomusic.all_music[name] = url
|
||||
xiaomusic.all_music_tags[name] = {
|
||||
"title": name,
|
||||
"artist": artist,
|
||||
"album": album,
|
||||
"year": "",
|
||||
"genre": "",
|
||||
"picture": picUrl,
|
||||
"lyrics": ""
|
||||
}
|
||||
|
||||
one_music_list.append(name)
|
||||
log.debug(f"getmy_playlist name:{list_name}")
|
||||
log.debug(one_music_list)
|
||||
# 歌曲名字相同会覆盖
|
||||
xiaomusic.music_list[list_name] = one_music_list
|
||||
xiaomusic.try_save_tag_cache()
|
||||
log.debug(xiaomusic.all_music)
|
||||
log.debug(xiaomusic.music_list)
|
||||
|
||||
return
|
||||
if playlist_id:
|
||||
songs_url = f"{api_host}/playlist/detail?id={playlist_id}"
|
||||
# 发起请求
|
||||
response = requests.get(songs_url, timeout=5) # 增加超时以避免长时间挂起
|
||||
response.raise_for_status() # 如果响应不是200,引发HTTPError异常
|
||||
# log.info(f"getmy_playlist url:{api_url} response:{response.text}")
|
||||
musics = response.json()
|
||||
list_name = musics['playlist']['name']
|
||||
one_music_list = []
|
||||
for music in musics['playlist']['tracks']:
|
||||
if (not music):
|
||||
continue
|
||||
# try:
|
||||
name = music['name']
|
||||
picUrl = music['al']['picUrl']
|
||||
artist = music['ar'][0]['name']
|
||||
album = music['al']['name']
|
||||
|
||||
name = music.get("name")
|
||||
url = f"{api_host}/song/url?id={music['id']}&br=350000&realIP=211.161.244.70&proxy=HTTP:%2F%2F127.0.0.1:8080"
|
||||
if (not name) or (not url):
|
||||
continue
|
||||
xiaomusic.all_music[name] = url
|
||||
xiaomusic.all_music_tags[name] = {
|
||||
"title": name,
|
||||
"artist": artist,
|
||||
"album": album,
|
||||
"year": "",
|
||||
"genre": "",
|
||||
"picture": picUrl,
|
||||
"lyrics": ""
|
||||
}
|
||||
one_music_list.append(name)
|
||||
log.debug(f"getmy_playlist name:{list_name}")
|
||||
log.debug(one_music_list)
|
||||
# 歌曲名字相同会覆盖
|
||||
xiaomusic.music_list[list_name] = one_music_list
|
||||
xiaomusic.try_save_tag_cache()
|
||||
log.debug(xiaomusic.all_music)
|
||||
log.debug(xiaomusic.music_list)
|
||||
return
|
||||
else:
|
||||
log.error(f"getmy_playlist type:{type} not support")
|
||||
|
||||
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 评论 9 - dissipator
|
||||
|
||||
> 等有空我写个修改歌单内容的插件示例吧。
|
||||
|
||||
不用拉,插件我已经写出来了
|
||||

|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 评论 10 - hanxi
|
||||
|
||||
发下你的 setting.json 配置吧,方便他人知道怎么配。
|
||||
|
||||
---
|
||||
|
||||
### 评论 11 - dissipator
|
||||
|
||||
我还在测试,设备到了能用了再发吧
|
||||
|
||||
---
|
||||
|
||||
### 评论 12 - guitarbug
|
||||
|
||||
也需要网易歌单功能, 坐等教程
|
||||
|
||||
---
|
||||
|
||||
### 评论 13 - dissipator
|
||||
|
||||
# 成功了
|
||||

|
||||
# 最好是在setting.json里去配置,我测试老是不匹配,在setting.json里去配置终于成功。可以直接调用 [NeteaseCloudMusicApi](http://localhost:3000/docs/#/?id=neteasecloudmusicapi) 接口,甚至使用UnblockNeteaseMusic 解锁灰色,但是代码需要小调整。
|
||||
|
||||
setting.json
|
||||
|
||||
```json
|
||||
{
|
||||
"account": "",
|
||||
"password": "",
|
||||
"mi_did": "",
|
||||
"miio_tts_command": "",
|
||||
"cookie": "",
|
||||
"verbose": false,
|
||||
"music_path": "music",
|
||||
"download_path": "music/download",
|
||||
"conf_path": "conf",
|
||||
"cache_dir": "cache",
|
||||
"hostname": "192.168.2.5",
|
||||
"port": 8090,
|
||||
"public_port": 0,
|
||||
"proxy": "",
|
||||
"search_prefix": "bilisearch:",
|
||||
"ffmpeg_location": "./ffmpeg/bin",
|
||||
"active_cmd": "play,set_random_play,playlocal,play_music_list,play_music_list_index,stop_after_minute,stop,获取歌单",
|
||||
"exclude_dirs": "@eaDir,tmp",
|
||||
"music_path_depth": 10,
|
||||
"disable_httpauth": true,
|
||||
"httpauth_username": "",
|
||||
"httpauth_password": "",
|
||||
"music_list_url": "",
|
||||
"music_list_json": "",
|
||||
"custom_play_list_json": "",
|
||||
"disable_download": false,
|
||||
"key_word_dict": {
|
||||
"播放歌曲": "play",
|
||||
"播放本地歌曲": "playlocal",
|
||||
"关机": "stop",
|
||||
"下一首": "play_next",
|
||||
"上一首": "play_prev",
|
||||
"单曲循环": "set_play_type_one",
|
||||
"全部循环": "set_play_type_all",
|
||||
"随机播放": "set_random_play",
|
||||
"分钟后关机": "stop_after_minute",
|
||||
"播放列表": "play_music_list",
|
||||
"刷新列表": "gen_music_list",
|
||||
"加入收藏": "add_to_favorites",
|
||||
"收藏歌曲": "add_to_favorites",
|
||||
"取消收藏": "del_from_favorites",
|
||||
"播放列表第": "play_music_list_index",
|
||||
"本地播放歌曲": "playlocal",
|
||||
"放歌曲": "play",
|
||||
"暂停": "stop",
|
||||
"停止": "stop",
|
||||
"停止播放": "stop",
|
||||
"播放歌单": "play_music_list",
|
||||
"测试自定义口令": "exec#code1(\"hello\")",
|
||||
"测试链接": "exec#httpget(\"https://github.com/hanxi/xiaomusic\")",
|
||||
"获取歌单": "exec#getmy_playlist(playlist_id=12758992225)"
|
||||
},
|
||||
"key_match_order": [
|
||||
"分钟后关机",
|
||||
"播放歌曲",
|
||||
"下一首",
|
||||
"上一首",
|
||||
"单曲循环",
|
||||
"全部循环",
|
||||
"随机播放",
|
||||
"关机",
|
||||
"刷新列表",
|
||||
"播放列表第",
|
||||
"播放列表",
|
||||
"加入收藏",
|
||||
"收藏歌曲",
|
||||
"取消收藏",
|
||||
"播放本地歌曲",
|
||||
"本地播放歌曲",
|
||||
"放歌曲",
|
||||
"暂停",
|
||||
"停止",
|
||||
"停止播放",
|
||||
"播放歌单",
|
||||
"测试自定义口令",
|
||||
"测试链接",
|
||||
"获取歌单"
|
||||
],
|
||||
"use_music_api": false,
|
||||
"use_music_audio_id": "1582971365183456177",
|
||||
"use_music_id": "355454500",
|
||||
"log_file": "/tmp/xiaomusic.txt",
|
||||
"fuzzy_match_cutoff": 0.6,
|
||||
"enable_fuzzy_match": true,
|
||||
"stop_tts_msg": "收到,再见",
|
||||
"enable_config_example": false,
|
||||
"keywords_playlocal": "播放本地歌曲,本地播放歌曲",
|
||||
"keywords_play": "播放歌曲,放歌曲",
|
||||
"keywords_stop": "关机,暂停,停止,停止播放",
|
||||
"keywords_playlist": "播放列表,播放歌单",
|
||||
"user_key_word_dict": {
|
||||
"测试自定义口令": "exec#code1(\"hello\")",
|
||||
"测试链接": "exec#httpget(\"https://github.com/hanxi/xiaomusic\")",
|
||||
"获取歌单": "exec#getmy_playlist(playlist_id=12758992225)"
|
||||
},
|
||||
"enable_force_stop": false,
|
||||
"devices": {
|
||||
" ": {
|
||||
"did": " ",
|
||||
"device_id": " -17c6-4204- - ",
|
||||
"hardware": "L05C",
|
||||
"name": "小黑你好",
|
||||
"play_type": "",
|
||||
"cur_music": "",
|
||||
"cur_playlist": ""
|
||||
}
|
||||
},
|
||||
"group_list": "",
|
||||
"remove_id3tag": false,
|
||||
"convert_to_mp3": false,
|
||||
"delay_sec": 3,
|
||||
"continue_play": false,
|
||||
"pull_ask_sec": 1,
|
||||
"crontab_json": "",
|
||||
"enable_yt_dlp_cookies": false,
|
||||
"get_ask_by_mina": false
|
||||
}
|
||||
```
|
||||
|
||||
getmy_playlist.py
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
async def getmy_playlist(type="netease",api_host="http://127.0.0.1/api", playlist_id=None,uid=None):
|
||||
"""
|
||||
Purpose:
|
||||
"""
|
||||
global log, xiaomusic
|
||||
if type == "netease":
|
||||
if uid:
|
||||
api_url = f"{api_host}/user/playlist?uid={uid}"
|
||||
# 发起请求
|
||||
response = requests.get(api_url, timeout=5) # 增加超时以避免长时间挂起
|
||||
response.raise_for_status() # 如果响应不是200,引发HTTPError异常
|
||||
# log.info(f"getmy_playlist url:{api_url} response:{response.text}")
|
||||
|
||||
music_list = response.json()
|
||||
for item in music_list['playlist']:
|
||||
list_name = item.get("name")
|
||||
|
||||
log.info(f"getmy_playlist name:{list_name}")
|
||||
# if item.get("id") in [12709941656,]:
|
||||
songs_url = f"{api_host}/playlist/detail?id={item['id']}"
|
||||
# 发起请求
|
||||
response = requests.get(songs_url, timeout=5) # 增加超时以避免长时间挂起
|
||||
response.raise_for_status() # 如果响应不是200,引发HTTPError异常
|
||||
# log.info(f"getmy_playlist url:{api_url} response:{response.text}")
|
||||
musics = response.json()
|
||||
one_music_list = []
|
||||
for music in musics['playlist']['tracks']:
|
||||
if (not music):
|
||||
continue
|
||||
# try:
|
||||
name = music['name']
|
||||
picUrl = music['al']['picUrl']
|
||||
artist = music['ar'][0]['name']
|
||||
album = music['al']['name']
|
||||
|
||||
name = music.get("name")
|
||||
url = f"{api_host}/song/url?id={music['id']}&br=350000&realIP=211.161.244.70&proxy=HTTP:%2F%2F127.0.0.1:8080"
|
||||
if (not name) or (not url):
|
||||
continue
|
||||
xiaomusic.all_music[name] = url
|
||||
xiaomusic.all_music_tags[name] = {
|
||||
"title": name,
|
||||
"artist": artist,
|
||||
"album": album,
|
||||
"year": "",
|
||||
"genre": "",
|
||||
"picture": picUrl,
|
||||
"lyrics": ""
|
||||
}
|
||||
|
||||
one_music_list.append(name)
|
||||
log.debug(f"getmy_playlist name:{list_name}")
|
||||
log.debug(one_music_list)
|
||||
# 歌曲名字相同会覆盖
|
||||
xiaomusic.music_list[list_name] = one_music_list
|
||||
xiaomusic.try_save_tag_cache()
|
||||
log.debug(xiaomusic.all_music)
|
||||
log.debug(xiaomusic.music_list)
|
||||
return
|
||||
if playlist_id:
|
||||
songs_url = f"{api_host}/playlist/detail?id={playlist_id}"
|
||||
# 发起请求
|
||||
response = requests.get(songs_url, timeout=5) # 增加超时以避免长时间挂起
|
||||
response.raise_for_status() # 如果响应不是200,引发HTTPError异常
|
||||
|
||||
musics = response.json()
|
||||
list_name = musics['playlist']['name']
|
||||
log.info(f"getmy_playlist list_name:{list_name} ")
|
||||
one_music_list = []
|
||||
for music in musics['playlist']['tracks']:
|
||||
if (not music):
|
||||
continue
|
||||
# try:
|
||||
name = music['name']
|
||||
picUrl = music['al']['picUrl']
|
||||
artist = music['ar'][0]['name']
|
||||
album = music['al']['name']
|
||||
|
||||
name = music.get("name")
|
||||
url = f"{api_host}/song/url?id={music['id']}&br=350000&proxy=HTTP:%2F%2F127.0.0.1:8080"
|
||||
if (not name) or (not url):
|
||||
continue
|
||||
xiaomusic.all_music[name] = url
|
||||
xiaomusic.all_music_tags[name] = {
|
||||
"title": name,
|
||||
"artist": artist,
|
||||
"album": album,
|
||||
"year": "",
|
||||
"genre": "",
|
||||
"picture": picUrl,
|
||||
"lyrics": ""
|
||||
}
|
||||
one_music_list.append(name)
|
||||
log.debug(f"getmy_playlist name:{list_name}")
|
||||
log.debug(one_music_list)
|
||||
# 歌曲名字相同会覆盖
|
||||
xiaomusic.music_list[list_name] = one_music_list
|
||||
xiaomusic.try_save_tag_cache()
|
||||
return
|
||||
else:
|
||||
log.error(f"getmy_playlist type:{type} not support")
|
||||
|
||||
```
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 评论 14 - dissipator
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### 评论 15 - dludream
|
||||
|
||||
NeteaseCloudMusicApi似乎获取不到播放地址?其他信息倒是有。
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### 评论 16 - hanxi
|
||||
|
||||
看代码是另一个接口获取url的: `url = f"{api_host}/song/url?id={music['id']}&br=350000&realIP=211.161.244.70&proxy=HTTP:%2F%2F127.0.0.1:8080"`
|
||||
|
||||
---
|
||||
|
||||
### 评论 17 - dludream
|
||||
|
||||
> 看代码是另一个接口获取url的: `url = f"{api_host}/song/url?id={music['id']}&br=350000&realIP=211.161.244.70&proxy=HTTP:%2F%2F127.0.0.1:8080"`
|
||||
|
||||
是的,我用的是这个接口,https://registry.hub.docker.com/r/gnehs/neteasecloudmusicapi-docker/
|
||||
|
||||
可能网易修改了api,这些获取不了播放地址了。
|
||||
|
||||
我倒不是要这个功能,只是感兴趣看看。我不获取,我直接用yt把歌全下载下来播放。
|
||||
|
||||
---
|
||||
|
||||
### 评论 18 - dissipator
|
||||
|
||||
这个不是网易的接口,是[NeteaseCloudMusicApi](http://localhost:3000/docs/#/?id=neteasecloudmusicapi) 接口。proxy=HTTP:%2F%2F127.0.0.1:8080"是UnblockNeteaseMusic 解锁灰色;
|
||||
完整的使用方式和docker 可以到 https://github.com/dissipator/xiaomusic/tree/dev 看README.md;目前没有教程。本人就在群2,有问题可以找我。
|
||||
|
||||
---
|
||||
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/269)
|
||||
45
docs/issues/285.md
Normal file
45
docs/issues/285.md
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
title: 相关工具推荐
|
||||
---
|
||||
|
||||
# 相关工具推荐
|
||||
|
||||
- [xhongc/music-tag-web 刮削音乐歌词图片](https://github.com/xhongc/music-tag-web)
|
||||
- [onlyLTY/dockerCopilot 一键更新容器](https://github.com/onlyLTY/dockerCopilot)
|
||||
|
||||
## 评论
|
||||
|
||||
|
||||
### 评论 1 - F-loat
|
||||
|
||||
借楼加个小程序的图 :partying_face: https://github.com/F-loat/xiaoplayer
|
||||
|
||||
### 小程序码
|
||||
|
||||
<p>
|
||||
<img alt="weapp" src="https://assets-1251785959.cos.ap-beijing.myqcloud.com/xiaoplayer/weappcode.jpg" width="24%" />
|
||||
</p>
|
||||
|
||||
### 截图
|
||||
|
||||
<p>
|
||||
<img src="https://assets-1251785959.cos.ap-beijing.myqcloud.com/xiaoplayer/screenshot/1.jpg" width="24%" />
|
||||
<img src="https://assets-1251785959.cos.ap-beijing.myqcloud.com/xiaoplayer/screenshot/2.jpg" width="24%" />
|
||||
<img src="https://assets-1251785959.cos.ap-beijing.myqcloud.com/xiaoplayer/screenshot/4.jpg" width="24%" />
|
||||
<img src="https://assets-1251785959.cos.ap-beijing.myqcloud.com/xiaoplayer/screenshot/3.jpg" width="24%" />
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
### 评论 2 - hanxi
|
||||
|
||||
@F-loat 可以在欢迎页加个链接显示小程序码。
|
||||
|
||||
---
|
||||
|
||||
### 评论 3 - F-loat
|
||||
|
||||
@hanxi 可以的,这样还能自动把 ip 用参数带过来,我有空搞一下
|
||||
|
||||
---
|
||||
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/285)
|
||||
62
docs/issues/294.md
Normal file
62
docs/issues/294.md
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
title: 关于M01型号的注意事项
|
||||
---
|
||||
|
||||
# 关于M01型号的注意事项
|
||||
|
||||
M01:在0.3.55版本【型号兼容模式】与【特殊型号获取对话记录】都设为false或true,都可以语音了。
|
||||
如果【型号兼容模式】为 true,默认UI显示播放中,但音箱没声音。
|
||||
|
||||
型号:S12A、LX04、S12 在米家APP可以联动,比如客厅有人自定义指令:播放歌曲、关机...等
|
||||
而M01无论【型号兼容模式】与【特殊型号获取对话记录】设为false或true,都无法执行任何自定义指令…
|
||||
|
||||

|
||||
|
||||
|
||||
## 评论
|
||||
|
||||
|
||||
### 评论 1 - hanxi
|
||||
|
||||
M01 保持默认设置应该是能语音和播放的吧。自定义指令的功能应该是 M01 本身就不支持,可能属于放弃维护的产品吧。
|
||||
|
||||
---
|
||||
|
||||
### 评论 2 - bj803
|
||||
|
||||
> M01 保持默认设置应该是能语音和播放的吧。自定义指令的功能应该是 M01 本身就不支持,可能属于放弃维护的产品吧。
|
||||
|
||||
刚试了用:pause是可以暂停(不过只暂2分钟左右又自己播放了)
|
||||
|
||||
---
|
||||
|
||||
### 评论 3 - hanxi
|
||||
|
||||
只能网页里点关机按钮,或者语音关机。所有型号都一样。
|
||||
|
||||
---
|
||||
|
||||
### 评论 4 - bj803
|
||||
|
||||
> 型号:S12A、LX04、S12
|
||||
|
||||
|
||||
|
||||
> 只能网页里点关机按钮,或者语音关机。所有型号都一样。
|
||||
|
||||
在型号:S12A、LX04、S12 除了能网页里点关机按钮或者语音开关机外,能在米家APP自定义指令进行播放与关机(例如当客厅客有人自定义指令"播放歌曲",无人自定义指令"关机"。M01自定义指令"pause"可以暂停,其他口令不行。
|
||||
|
||||
---
|
||||
|
||||
### 评论 5 - bj803
|
||||
|
||||
刚又试了一下,用自定义play stop、power off可以停止播放关机,用自定义play music可以播放音乐,可能M01不需要下划线
|
||||
|
||||
---
|
||||
|
||||
### 评论 6 - hanxi
|
||||
|
||||
感谢你的反馈。
|
||||
|
||||
---
|
||||
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/294)
|
||||
250
docs/issues/297.md
Normal file
250
docs/issues/297.md
Normal file
@@ -0,0 +1,250 @@
|
||||
---
|
||||
title: xiaomusic极空间安装教程(2024/12/28更新)
|
||||
---
|
||||
|
||||
# xiaomusic极空间安装教程(2024-12-28更新)
|
||||
|
||||
> 本教程同步更新于最新版的xiaomusic
|
||||
|
||||
<s>看不懂/嫌麻烦/懒 但有点小钱,找 hanxi 预约微信或者 QQ 远程安装,他便宜,收费50一次,作法不成功不要钱</s>
|
||||
|
||||
**ARM架构自己想办法获取镜像 点名Z2PRO**
|
||||
|
||||
# 获取镜像
|
||||
|
||||
## 科学环境:
|
||||
|
||||
1. 在 **搜索框** 中输入 `hanxi/xiaomusic`,在搜索的结果中直接选择第一个,点击**下载**
|
||||
|
||||

|
||||
|
||||
2. 在新弹出的版本选择窗口中,根据你的情况选择。
|
||||

|
||||
|
||||
### 版本说明
|
||||
- 获取 **最新版** 直接点击 **下载** 即可,建议使用默认的 `latest`
|
||||
- 获取 **特定版本** [点击此处可查看](https://github.com/hanxi/xiaomusic/releases) 用于回退出现功能不兼容、恶性bug等情况,一般建议反馈开发者,修复很及时,尽量不要回退版本 请输入如 `v0.3.55`
|
||||
- 获取 **实验版本**(已修复部分bug但未推送)请输入 `main`
|
||||
|
||||
|
||||
3. 接着弹出如图所示的页面,耐心等待下载完成。
|
||||

|
||||
|
||||
|
||||
4. 下载完成后切换到 **本地镜像** 选项卡
|
||||
|
||||
剩余步骤与国内环境相同,见 [部署镜像](#部署镜像)
|
||||
|
||||
## 国内环境:
|
||||
1. 打开docker,在左侧的菜单中选择 **镜像** 切换到 **仓库** 选项卡,点击 **自定义拉取** 按钮
|
||||

|
||||
2. 在弹出的对话框中输入 ` m.daocloud.io/docker.io/hanxi/xiaomusic ` ,点击 **拉取** 按钮
|
||||

|
||||
3. 下载完成后切换到 **本地镜像** 选项卡
|
||||
|
||||
# 部署镜像
|
||||
1. 找到刚才已经拉取好的镜像,*单击选中*,点击 **添加到容器**
|
||||

|
||||
2. 在弹出的 **创建容器** 菜单中,切换到 **文件夹路径** 选项卡中,按图中的提示进行配置。
|
||||

|
||||
|
||||
**注意:**
|
||||
* 装载路径中的 **配置文件目录** 和 **音乐目录** 必须进行配置,**其他目录非必要请勿配置**
|
||||
* 主题目录为方便开发主题调试时的配置选项,普通用户不能理解明确用途请**不要配置主题目录**,否则会报**HTTP Status 500 – Internal Server Error** 错误
|
||||
* 如有多个音乐目录,请按照下面的格式进行配置
|
||||
|
||||
| 文件/文件夹 | 装载路径 |
|
||||
| :----------: | :----------: |
|
||||
| /data/music1 | /app/music/music1 |
|
||||
| /data/music2 | /app/music/music2 |
|
||||
|
||||
3. 切换到 **端口** 选项卡,修改成与你的极空间 *不冲突* 的本地端口号,如 `5678` (示例按照本地端口号5678来进行配置,下同)
|
||||
> 友情提醒: 尽量不要修改容器端口号,否则要到配置文件目录修改对应的`setting.json`文件中的配置,会增加很多麻烦
|
||||
|
||||

|
||||
|
||||
5. 切换到 **环境** 选项卡,将`XIAOMUSIC_HOSTNAME` 修改为你的 **极空间的IP地址**
|
||||
> 友情提醒:
|
||||
> 1. 此处不可忽略,否则后续播放音乐会出现问题
|
||||
> 2. 不要尝试修改XIAOMUSIC_PORT!除非你没有看上一条的友情提醒
|
||||
> 3. 不要在此处配置`ACCOUNT`和`PASSWORD`,没有过风控仍然无法使用!上古时代的教程不要再看了,容易走火入魔!
|
||||
|
||||

|
||||
|
||||
6. 点击 **应用**按钮,此时容器已经配置完成了,切换到左侧的 **容器概况** 菜单,可查看容器详情
|
||||

|
||||
|
||||
# 进入xiaomusic网页端进行配置
|
||||
1.请关闭代理,打开浏览器,地址栏输入 **极空间IP:本地端口号** 如`192.168.2.5:5678`,打开网页后点击 **默认主题**
|
||||
|
||||

|
||||
|
||||
**注意:**
|
||||
* 不要复制此处的地址,必须输入极空间的IP地址。不知道的建议上咸鱼50块换个不锈钢盆
|
||||
* 不要输入容器的端口号8090,极空间不能使用这个端口号。
|
||||
|
||||
2. 点击 **设置** 按钮进入设置页面
|
||||

|
||||
|
||||
3. 输入**小米账号**、**小米密码**、**XIAOMUSIC_HOSTNAME(IP或域名):**、**外网访问端口**,滑到页面最下方点击 **保存**
|
||||

|
||||

|
||||
|
||||
**注意:**
|
||||
* 小米账号非手机号,请在手机设置-个人中心中查看小米ID
|
||||
* 密码不要输错,账号密码错误在上面会弹出提醒,不要假装看不见上面的提醒文字
|
||||
* XIAOMUSIC_HOSTNAME(IP或域名): 可以输入当前页面的IP地址(在地址栏),**不要在此处输入端口号!!!**,如果域名需要使用https协议,请加上https://
|
||||
|
||||
4.如果以上步骤没错,你将在设置中心看见设备列表
|
||||

|
||||
|
||||
5. 回到首页,出现设备列表,切换对应设备即可畅享
|
||||

|
||||
|
||||
|
||||
## 评论
|
||||
|
||||
|
||||
### 评论 1 - xiaohuobanhahaha
|
||||
|
||||
[xiaomusic.txt](https://github.com/user-attachments/files/18011572/xiaomusic.txt)
|
||||
|
||||
<img width="559" alt="截屏2024-12-05 00 43 24" src="https://gproxy.hanxi.cc/proxy/user-attachments/assets/160aeacc-e1c0-40fa-b219-6b6f5183c43c">
|
||||
an'zh
|
||||
无法使用语音播放歌曲,小爱s12a。极空间z4pro。
|
||||
1. 按照教程,点击播放本地歌曲,提示hostname和设置的端口映射不匹配。映射5678,容器端口8090.
|
||||
2.网络host模式,能够本地播放,无法使用语音控制,提示“下载app”。日志已上传
|
||||
|
||||
---
|
||||
|
||||
### 评论 2 - 52fisher
|
||||
|
||||
> [xiaomusic.txt](https://github.com/user-attachments/files/18011572/xiaomusic.txt)
|
||||
> <img alt="截屏2024-12-05 00 43 24" width="559" src="https://private-user-images.githubusercontent.com/20666294/392485798-160aeacc-e1c0-40fa-b219-6b6f5183c43c.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzMzMzE3NzIsIm5iZiI6MTczMzMzMTQ3MiwicGF0aCI6Ii8yMDY2NjI5NC8zOTI0ODU3OTgtMTYwYWVhY2MtZTFjMC00MGZhLWIyMTktNmI2ZjUxODNjNDNjLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDEyMDQlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQxMjA0VDE2NTc1MlomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWJhMmZjMzFhMzdmYmI1NGJjZTg1ZGVhNGI2Njc1YjYwYmQxZjVmMzYyYjg3YWNlNzdhNmEwMzE4Y2UyMTRlNjUmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0._OSLH4Bc0stzG4tLdPalW-mB-ASKlM9uztZ1icefPFU"> an'zh 无法使用语音播放歌曲,小爱s12a。极空间z4pro。 1. 按照教程,点击播放本地歌曲,提示hostname和设置的端口映射不匹配。映射5678,容器端口8090. 2.网络host模式,能够本地播放,无法使用语音控制,提示“下载app”。日志已上传
|
||||
|
||||
既然你映射的5678,为什么你又在那把监听端口改成11999? 我的教程里面全程没有写要修改监听端口
|
||||
|
||||
---
|
||||
|
||||
### 评论 3 - xiaohuobanhahaha
|
||||
|
||||
我没讲清楚。我试了两种极空间的桥接和host模式。桥接模式。我按照教程走的。报错如图
|
||||
<img width="847" alt="截屏2024-12-05 01 46 52" src="https://gproxy.hanxi.cc/proxy/user-attachments/assets/e7f58907-a216-41e8-bafa-5d49db8eca45">
|
||||
<img width="516" alt="截屏2024-12-05 01 49 11" src="https://gproxy.hanxi.cc/proxy/user-attachments/assets/4261c2e2-fe0c-4ff6-ae06-ead7f928af57">
|
||||
<img width="647" alt="截屏2024-12-05 01 47 02" src="https://gproxy.hanxi.cc/proxy/user-attachments/assets/35b195d1-9512-40bb-b336-847e0bb2e6c9">
|
||||
<img width="667" alt="截屏2024-12-05 01 47 15" src="https://gproxy.hanxi.cc/proxy/user-attachments/assets/b917a977-38cf-4126-8754-c46abe9360a2">
|
||||
|
||||
提到的第二个问题和日志,是我将网络模式改为host的情况,能连上音箱,但是没法使用语音控制。
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 评论 4 - 52fisher
|
||||
|
||||
> 我没讲清楚。我试了两种极空间的桥接和host模式。桥接模式。我按照教程走的。报错如图 <img alt="截屏2024-12-05 01 46 52" width="847" src="https://private-user-images.githubusercontent.com/20666294/392507064-e7f58907-a216-41e8-bafa-5d49db8eca45.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzMzMzQ4OTEsIm5iZiI6MTczMzMzNDU5MSwicGF0aCI6Ii8yMDY2NjI5NC8zOTI1MDcwNjQtZTdmNTg5MDctYTIxNi00MWU4LWJhZmEtNWQ0OWRiOGVjYTQ1LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDEyMDQlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQxMjA0VDE3NDk1MVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTBlMTYxZGI0ZjRiYTkyOGIwZWQ4YWZlZWJiY2U3MzljZTg5NTNhZjJkNzlkNzYyYzlmMWJkZjlkMGYwNWEzNzgmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.XSsE7pZj7pwj4kaZBQ02fyH13bVXLzGuJcjRv98WWiQ"> <img alt="截屏2024-12-05 01 49 11" width="516" src="https://private-user-images.githubusercontent.com/20666294/392507855-4261c2e2-fe0c-4ff6-ae06-ead7f928af57.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzMzMzQ4OTEsIm5iZiI6MTczMzMzNDU5MSwicGF0aCI6Ii8yMDY2NjI5NC8zOTI1MDc4NTUtNDI2MWMyZTItZmUwYy00ZmY2LWFlMDYtZWFkN2Y5MjhhZjU3LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDEyMDQlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQxMjA0VDE3NDk1MVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTk2NzFjYTRmNzAyMTJlYzYwZDI4NThlMWQ1MDViZGU5MDI3YThjNzExZTAyNWJmM2NlYWQzZDIzYzRhMjc1MTcmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.NIBWfzpZ5OTIic6Gv83_PrOdUX27o19Vo1zDvFyrILE"> <img alt="截屏2024-12-05 01 47 02" width="647" src="https://private-user-images.githubusercontent.com/20666294/392507111-35b195d1-9512-40bb-b336-847e0bb2e6c9.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzMzMzQ4OTEsIm5iZiI6MTczMzMzNDU5MSwicGF0aCI6Ii8yMDY2NjI5NC8zOTI1MDcxMTEtMzViMTk1ZDEtOTUxMi00MGJiLWIzMzYtODQ3ZTBiYjJlNmM5LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDEyMDQlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQxMjA0VDE3NDk1MVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWI5YTliOWQzNDUyYThjODYwZmY4NTJiMTNkYmFmNmY3ZWE5ZDBlMmQ5OGQxMTIzM2JlMmIxZTcwNTNlMmYwZTEmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.f9czCn43Yzm7sn6-cpJykaWIng4WJf9aoE52kbVeASY"> <img alt="截屏2024-12-05 01 47 15" width="667" src="https://private-user-images.githubusercontent.com/20666294/392507187-b917a977-38cf-4126-8754-c46abe9360a2.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzMzMzQ4OTEsIm5iZiI6MTczMzMzNDU5MSwicGF0aCI6Ii8yMDY2NjI5NC8zOTI1MDcxODctYjkxN2E5NzctMzhjZi00MTI2LTg3NTQtYzQ2YWJlOTM2MGEyLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDEyMDQlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQxMjA0VDE3NDk1MVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWQ3MDE1NjVhZjMzMTRkNjg5ZTA5NDc1MDU3OTFiODc3NTYyMTg3Y2FjNjg2NGM3MjE0N2VlNjg0YzFmZTgwZGImWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.tX8iIlpvE43Krq4q__7dQ3Wuz8kYQuzmlf-XXNHO1Ws">
|
||||
>
|
||||
> 提到的第二个问题和日志,是我将网络模式改为host的情况,能连上音箱,但是没法使用语音控制。
|
||||
|
||||
把外网访问端口改成5678
|
||||
|
||||
---
|
||||
|
||||
### 评论 5 - xiaohuobanhahaha
|
||||
|
||||
可以连接上网页控制了,但是语音控制仍然不行。已经按照FAQ问题集合 #99 重启docker。
|
||||
这是日志,
|
||||
[xiaomusic.txt](https://github.com/user-attachments/files/18012369/xiaomusic.txt)
|
||||
|
||||
---
|
||||
|
||||
### 评论 6 - 52fisher
|
||||
|
||||
> 可以连接上网页控制了,但是语音控制仍然不行。已经按照FAQ问题集合 #99 重启docker。 这是日志, [xiaomusic.txt](https://github.com/user-attachments/files/18012369/xiaomusic.txt)
|
||||
|
||||
|
||||
你的设置 hostname='192.168.31.165', port=8090, public_port=5678,
|
||||
后台的ip 192.168.31.143 检查一下你的地址 有可能是你的ip地址改变了
|
||||
|
||||
---
|
||||
|
||||
### 评论 7 - xiaohuobanhahaha
|
||||
|
||||
> > 可以连接上网页控制了,但是语音控制仍然不行。已经按照FAQ问题集合 #99 重启docker。 这是日志, [xiaomusic.txt](https://github.com/user-attachments/files/18012369/xiaomusic.txt)
|
||||
>
|
||||
> 你的设置 hostname='192.168.31.165', port=8090, public_port=5678, 后台的ip 192.168.31.143 检查一下你的地址 有可能是你的ip地址改变了
|
||||
|
||||
确实是变了。192.168.31.143是我电脑的ip。 hostname='192.168.31.165'是极空间的。小爱是192.168.31.77。现在我的网络结构是电脑连nas上的istoreos旁路由。nas直连主路由,小爱直连主路由。主路由dhcp都绑定了。 大佬,这种情况该怎么解决呢。所有设置都是默认,没修改哈。
|
||||
|
||||
---
|
||||
|
||||
### 评论 8 - 52fisher
|
||||
|
||||
> 确实是变了。192.168.31.143是我电脑的ip。 hostname='192.168.31.165'是极空间的。小爱是192.168.31.77。现在我的网络结构是电脑连nas上的istoreos旁路由。nas直连主路由,小爱直连主路由。主路由dhcp都绑定了。 大佬,这种情况该怎么解决呢。所有设置都是默认,没修改哈。
|
||||
|
||||
容器的网络模式改成bridge试试呢 没解决的话你加群明天再详聊吧
|
||||
|
||||
---
|
||||
|
||||
### 评论 9 - xiaohuobanhahaha
|
||||
|
||||
<img width="660" alt="截屏2024-12-05 02 23 53" src="https://gproxy.hanxi.cc/proxy/user-attachments/assets/b9d26de9-3dcf-4e65-9460-36603735c887">
|
||||
<img width="780" alt="截屏2024-12-05 02 24 49" src="https://gproxy.hanxi.cc/proxy/user-attachments/assets/6a204cdb-bb10-4f35-822d-613aeed0fae0">
|
||||
|
||||
|
||||
> > 确实是变了。192.168.31.143是我电脑的ip。 hostname='192.168.31.165'是极空间的。小爱是192.168.31.77。现在我的网络结构是电脑连nas上的istoreos旁路由。nas直连主路由,小爱直连主路由。主路由dhcp都绑定了。 大佬,这种情况该怎么解决呢。所有设置都是默认,没修改哈。
|
||||
>
|
||||
> 容器的网络模式改成bridge试试呢 没解决的话你加群明天再详聊吧
|
||||
|
||||
辛苦了,这么晚还在回复。我一直用的bridge。大佬,群号多少,不行我明天群里问吧。
|
||||
|
||||
---
|
||||
|
||||
### 评论 10 - 52fisher
|
||||
|
||||
> <img alt="截屏2024-12-05 02 23 53" width="660" src="https://private-user-images.githubusercontent.com/20666294/392519509-b9d26de9-3dcf-4e65-9460-36603735c887.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzMzMzcwNzEsIm5iZiI6MTczMzMzNjc3MSwicGF0aCI6Ii8yMDY2NjI5NC8zOTI1MTk1MDktYjlkMjZkZTktM2RjZi00ZTY1LTk0NjAtMzY2MDM3MzVjODg3LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDEyMDQlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQxMjA0VDE4MjYxMVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWFhN2I4NzQyYjA4YzRiOTk4OTU3NmVkNjU2MjM1ODhjMmNlOWU0YTg5OTFlMzA1NTcxMTA5OTk1YjgwNThhOTgmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.yrC-sn2T6PKwBJb_mal1T2yxcSz008Hb7KWVmOe6zaA"> <img alt="截屏2024-12-05 02 24 49" width="780" src="https://private-user-images.githubusercontent.com/20666294/392519852-6a204cdb-bb10-4f35-822d-613aeed0fae0.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzMzMzcwNzEsIm5iZiI6MTczMzMzNjc3MSwicGF0aCI6Ii8yMDY2NjI5NC8zOTI1MTk4NTItNmEyMDRjZGItYmIxMC00ZjM1LTgyMmQtNjEzYWVlZDBmYWUwLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDEyMDQlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQxMjA0VDE4MjYxMVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTE4MzlhODM1NDY4ZjFmN2I2Y2JkZWU5ZGFkNTNjYTNmZDg2OTU3YzA0MjRkMDA2MzRmOTk2ZGE3NmE2NjViZmImWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0._I8Rll2a96i-cx6WBvEwOclNInfOmZkD5HmcorzT-KI">
|
||||
>
|
||||
> > > 确实是变了。192.168.31.143是我电脑的ip。 hostname='192.168.31.165'是极空间的。小爱是192.168.31.77。现在我的网络结构是电脑连nas上的istoreos旁路由。nas直连主路由,小爱直连主路由。主路由dhcp都绑定了。 大佬,这种情况该怎么解决呢。所有设置都是默认,没修改哈。
|
||||
> >
|
||||
> >
|
||||
> > 容器的网络模式改成bridge试试呢 没解决的话你加群明天再详聊吧
|
||||
>
|
||||
> 辛苦了,这么晚还在回复。我一直用的bridge。大佬,群号多少,不行我明天群里问吧。
|
||||
|
||||
[readme](https://github.com/hanxi/xiaomusic?tab=readme-ov-file#-%E8%AE%A8%E8%AE%BA%E5%8C%BA)
|
||||
|
||||
---
|
||||
|
||||
### 评论 11 - xiaohuobanhahaha
|
||||
|
||||
> > <img alt="截屏2024-12-05 02 23 53" width="660" src="https://private-user-images.githubusercontent.com/20666294/392519509-b9d26de9-3dcf-4e65-9460-36603735c887.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzMzMzcwNzEsIm5iZiI6MTczMzMzNjc3MSwicGF0aCI6Ii8yMDY2NjI5NC8zOTI1MTk1MDktYjlkMjZkZTktM2RjZi00ZTY1LTk0NjAtMzY2MDM3MzVjODg3LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDEyMDQlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQxMjA0VDE4MjYxMVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWFhN2I4NzQyYjA4YzRiOTk4OTU3NmVkNjU2MjM1ODhjMmNlOWU0YTg5OTFlMzA1NTcxMTA5OTk1YjgwNThhOTgmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.yrC-sn2T6PKwBJb_mal1T2yxcSz008Hb7KWVmOe6zaA"> <img alt="截屏2024-12-05 02 24 49" width="780" src="https://private-user-images.githubusercontent.com/20666294/392519852-6a204cdb-bb10-4f35-822d-613aeed0fae0.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzMzMzcwNzEsIm5iZiI6MTczMzMzNjc3MSwicGF0aCI6Ii8yMDY2NjI5NC8zOTI1MTk4NTItNmEyMDRjZGItYmIxMC00ZjM1LTgyMmQtNjEzYWVlZDBmYWUwLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDEyMDQlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQxMjA0VDE4MjYxMVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTE4MzlhODM1NDY4ZjFmN2I2Y2JkZWU5ZGFkNTNjYTNmZDg2OTU3YzA0MjRkMDA2MzRmOTk2ZGE3NmE2NjViZmImWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0._I8Rll2a96i-cx6WBvEwOclNInfOmZkD5HmcorzT-KI">
|
||||
> > > > 确实是变了。192.168.31.143是我电脑的ip。 hostname='192.168.31.165'是极空间的。小爱是192.168.31.77。现在我的网络结构是电脑连nas上的istoreos旁路由。nas直连主路由,小爱直连主路由。主路由dhcp都绑定了。 大佬,这种情况该怎么解决呢。所有设置都是默认,没修改哈。
|
||||
> > >
|
||||
> > >
|
||||
> > > 容器的网络模式改成bridge试试呢 没解决的话你加群明天再详聊吧
|
||||
> >
|
||||
> >
|
||||
> > 辛苦了,这么晚还在回复。我一直用的bridge。大佬,群号多少,不行我明天群里问吧。
|
||||
>
|
||||
> [readme](https://github.com/hanxi/xiaomusic?tab=readme-ov-file#-%E8%AE%A8%E8%AE%BA%E5%8C%BA)
|
||||
|
||||
已自查解决。问题是账号问题。绑定设备的一定是创建者,不能是管理员。
|
||||
|
||||
---
|
||||
|
||||
### 评论 12 - McCree2020
|
||||
|
||||
这个主题目录不能设置吧?没人遇到这个issue?我原来用的张大妈平台的教程设置的,能用,后来看到这个教程后就修改了后台的路径映射,但是dockers启动正常,网页不能打开提示internal sever error,后来ssh进docker看了日志文件 提示static那个路径有问题,下边的index什么的文件找不到, 删除主题映射以后,重启docker后,网页正常显示了
|
||||
|
||||
---
|
||||
|
||||
### 评论 13 - 52fisher
|
||||
|
||||
> 这个主题目录不能设置吧?没人遇到这个issue?我原来用的张大妈平台的教程设置的,能用,后来看到这个教程后就修改了后台的路径映射,但是dockers启动正常,网页不能打开提示internal sever error,后来ssh进docker看了日志文件 提示static那个路径有问题,下边的index什么的文件找不到, 删除主题映射以后,重启docker后,网页正常显示了
|
||||
|
||||
要注意看提示:
|
||||
装载路径中的 配置文件目录 和 音乐目录 必须进行配置。
|
||||
|
||||
其他的路径非必要不要配置,主题目录路径是方便开发调试的时候用的,普通用户不要映射主题目录
|
||||
|
||||
---
|
||||
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/297)
|
||||
31
docs/issues/333.md
Normal file
31
docs/issues/333.md
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
title: 设置项功能介绍
|
||||
---
|
||||
|
||||
# 设置项功能介绍
|
||||
|
||||
- XIAOMUSIC_ACTIVE_CMD 环境变量,对应后台的 【允许唤醒的命令】,用于唤醒口令,配置成'play,random_play',在非播放状态下,只有这两个指令(播放歌曲和随机播放)可以触发,触发后,xiaomusic进入playing状态,其他指令则可以正常触发。具体见 <https://github.com/hanxi/xiaomusic/pull/43>
|
||||
- XIAOMUSIC_EXCLUDE_DIRS 配置歌曲目录里需要忽略的目录,对应后台的 【忽略目录】
|
||||
- XIAOMUSIC_MUSIC_PATH_DEPTH 配置歌曲目录搜索深度,对应后台的 【目录深度】,具体见 </issues/76.html>
|
||||
- XIAOMUSIC_DISABLE_HTTPAUTH 配置成 false 表示开启密码访问web控制台,对应后台的 【关闭密码验证】,具体见 </issues/47.html>
|
||||
- XIAOMUSIC_HTTPAUTH_USERNAME 配置 web 控制台用户,对应后台的 【控制台账户】
|
||||
- XIAOMUSIC_HTTPAUTH_PASSWORD 配置 web 控制台密码,对应后台的 【控制台密码】
|
||||
- XIAOMUSIC_CONF_PATH 用来存放配置文件的目录,对应后台的 【配置文件目录】,记得把目录映射到主机,默认为 `/app/config` ,具体见 </issues/74.html>
|
||||
- XIAOMUSIC_CACHE_DIR 用来音乐 tag 缓存,默认为 `/app/cache`,对应后台的 【缓存文件目录】。
|
||||
- XIAOMUSIC_DISABLE_DOWNLOAD 设为 true 时关闭下载功能,对应后台的 【关闭下载功能】,见 </issues/82.html>
|
||||
- XIAOMUSIC_USE_MUSIC_API 设为 true 时使用 player_play_music 接口播放音乐,对应后台的 【型号兼容模式】,用于兼容不能播放的型号,如果发现需要设置这个选项的时候请告知我加一下设备型号,方便以后不用设置。 见 </issues/30.html>
|
||||
- XIAOMUSIC_KEYWORDS_PLAY 用来播放歌曲的口令前缀,对应后台的 【播放歌曲口令】,默认是 "播放歌曲,放歌曲" ,可以用英文逗号分割配置多个
|
||||
- XIAOMUSIC_KEYWORDS_STOP 用来关机的口令,对应后台的 【停止口令】,默认是 "关机,暂停,停止" ,可以用英文逗号分割配置多个。
|
||||
- XIAOMUSIC_KEYWORDS_PLAYLOCAL 用来播放本地歌曲的口令前缀,对应后台的 【播放本地歌曲口令】,本地找不到时不会下载歌曲,默认是 "播放本地歌曲,本地播放歌曲" ,可以用英文逗号分割配置多个。
|
||||
- XIAOMUSIC_ENABLE_FUZZY_MATCH 设为 true 时开启模糊匹配(默认),设为 false 时关闭模糊匹配,对应后台的 【开启模糊搜索】,支持模糊匹配歌名和歌单名。 具体见 </issues/52.html>
|
||||
- XIAOMUSIC_FUZZY_MATCH_CUTOFF 设置模糊搜索匹配的最低相似度阈值(默认0.6,可以配0到1直接的小数),越小越模糊,越大越精准,对应后台的 【模糊匹配阈值】。具体见 </issues/52.html>
|
||||
- XIAOMUSIC_PUBLIC_PORT 用于设置外网端口,对应后台的 【外网访问端口】,当使用反向代理时可以设置为外网端口,XIAOMUSIC_HOSTNAME 设为外网IP或者域名即可。
|
||||
- XIAOMUSIC_DOWNLOAD_PATH 变量可以配置下载目录,默认为空,表示使用 music 目录为下载目录,对应后台的 【音乐下载目录】。设置这个目录必须是 music 的子目录,否则刷新列表后会找不到歌曲。具体见 </issues/98.html>
|
||||
- XIAOMUSIC_PROXY 用于配置国内使用 youtube 源下载歌曲时使用的代理,参数格式参考 yt-dlp 文档说明。 见 </issues/2.html> 和 </issues/11.html>
|
||||
- MIIO_TTS_CMD 用于部分机型(如:`L05C`)使用 MiIO 支持 tts 能力,默认为空,命令选择见 [MiService-fork 文档](https://github.com/yihong0618/MiService)
|
||||
|
||||
|
||||
## 评论
|
||||
|
||||
没有评论。
|
||||
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/333)
|
||||
392
docs/issues/78.md
Normal file
392
docs/issues/78.md
Normal file
@@ -0,0 +1,392 @@
|
||||
---
|
||||
title: 已支持配置自定义网络歌单,在这里分享你的歌单
|
||||
---
|
||||
|
||||
# 已支持配置自定义网络歌单,在这里分享你的歌单
|
||||
|
||||
设置页面新增一个输入框配置json格式,可以定义配置音乐源,可以是电台或者其他的m3u8格式的。
|
||||
再加一个输入框配置这个json文件的url,点击获取按钮把url对应的json内容填充到json输入框,方便直接使用别人分享的歌单。
|
||||
|
||||
比如这样的链接
|
||||
- https://lhttp.qtfm.cn/live/4915/64k.mp3
|
||||
- http://ngcdn001.cnr.cn/live/zgzs/index.m3u8
|
||||
|
||||
已经测试能播放出来:
|
||||
```
|
||||
python3 micli.py play http://ngcdn001.cnr.cn/live/zgzs/index.m3u8
|
||||
```
|
||||
|
||||
预计歌单格式是这样的, type 为 radio 作为电台的设定,会一直播放当前电台,不会播放下一首。
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name":"歌单1",
|
||||
"musics":[
|
||||
{
|
||||
"name":"歌名1",
|
||||
"url":"http://ngcdn001.cnr.cn/live/zgzs/index.m3u8",
|
||||
"type":"radio"
|
||||
},
|
||||
{
|
||||
"name":"歌名2",
|
||||
"url":"https://lhttp.qtfm.cn/live/4915/64k.mp3"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name":"歌单2",
|
||||
"musics":[
|
||||
{
|
||||
"name":"歌名3",
|
||||
"url":"https://lhttp.qtfm.cn/live/4915/64k.mp3"
|
||||
},
|
||||
{
|
||||
"name":"歌名4",
|
||||
"url":"https://lhttp.qtfm.cn/live/4915/64k.mp3"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
这里分享一个让 chatgpt 写 python 脚本来生成歌单的例子 <https://chatgpt.com/share/6751c019-74c0-800a-a978-a20c636d4464> 。
|
||||
|
||||
## 评论
|
||||
|
||||
|
||||
### 评论 1 - hanxi
|
||||
|
||||
可以使用 gist 来配置和分享 json 文件,比如 https://gist.github.com/hanxi/dda82d964a28f8110f8fba81c3ff8314
|
||||
|
||||
点击 raw 得到 json 文件的链接 https://gist.githubusercontent.com/hanxi/dda82d964a28f8110f8fba81c3ff8314/raw/8787844d81c39dbfaad4e37954dd449d8bba5728/example.json
|
||||
|
||||
当然还可以用其他工具分享json文件,比如 github 和国内的 gitee 。
|
||||
|
||||
---
|
||||
|
||||
### 评论 2 - hanxi
|
||||
|
||||
已经有工具支持将 m3u 格式的电台文件转为网络歌单格式,见 /issues/88.html
|
||||
|
||||
欢迎有兴趣的朋友制作其他格式转换工具,比如网易歌单那一类的。
|
||||
|
||||
---
|
||||
|
||||
### 评论 3 - lazybabyz
|
||||
|
||||
按照教程设置后 播放列表选择m3u电台 再选择播放列表 歌曲,结果显示播放中 不断在转台
|
||||
stdout: [08:58:02] [0.3.37] [INFO] 10.0.80.191:24020 - "GET /playingmusic?did=566731712 HTTP/1.1" 200
|
||||
stdout: [08:58:04] [0.3.37] [INFO] 10.0.80.191:24020 - "GET /playingmusic?did=566731712 HTTP/1.1" 200
|
||||
stdout: [08:58:04] [0.3.37] [INFO] 10.0.80.191:24020 - "GET /static/default/index.html HTTP/1.1" 304
|
||||
stdout: [08:58:04] [0.3.37] [INFO] 10.0.80.191:24020 - "GET /getsetting HTTP/1.1" 200
|
||||
stdout: [08:58:04] [0.3.37] [INFO] 10.0.80.191:24022 - "GET /getversion HTTP/1.1" 200
|
||||
stdout: [08:58:04] [0.3.37] [INFO] 10.0.80.191:24020 - "GET /musiclist HTTP/1.1" 200
|
||||
stdout: [08:58:04] [0.3.37] [INFO] 10.0.80.191:24021 - "GET /playingmusic?did=566731712 HTTP/1.1" 200
|
||||
stdout: [08:58:04] [0.3.37] [INFO] 10.0.80.191:24020 - "GET /static/default/curplaylist?did=566731712 HTTP/1.1" 404
|
||||
stdout: [08:58:05] [0.3.37] [INFO] 10.0.80.191:24022 - "GET /getvolume?did=566731712 HTTP/1.1" 500
|
||||
stderr: [08:58:05] [0.3.37] [ERROR] Exception in ASGI application
|
||||
stderr: Traceback (most recent call last):
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/uvicorn/protocols/http/h11_impl.py", line 406, in run_asgi
|
||||
stderr: result = await app( # type: ignore[func-returns-value]
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/uvicorn/middleware/proxy_headers.py", line 70, in __call__
|
||||
stderr: return await self.app(scope, receive, send)
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/fastapi/applications.py", line 1054, in __call__
|
||||
stderr: await super().__call__(scope, receive, send)
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/applications.py", line 113, in __call__
|
||||
stderr: await self.middleware_stack(scope, receive, send)
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/middleware/errors.py", line 187, in __call__
|
||||
stderr: raise exc
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/middleware/errors.py", line 165, in __call__
|
||||
stderr: await self.app(scope, receive, _send)
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/middleware/cors.py", line 85, in __call__
|
||||
stderr: await self.app(scope, receive, send)
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 62, in __call__
|
||||
stderr: await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/_exception_handler.py", line 62, in wrapped_app
|
||||
stderr: raise exc
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/_exception_handler.py", line 51, in wrapped_app
|
||||
stderr: await app(scope, receive, sender)
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/routing.py", line 715, in __call__
|
||||
stderr: await self.middleware_stack(scope, receive, send)
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/routing.py", line 735, in app
|
||||
stderr: await route.handle(scope, receive, send)
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/routing.py", line 288, in handle
|
||||
stderr: await self.app(scope, receive, send)
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/routing.py", line 76, in app
|
||||
stderr: await wrap_app_handling_exceptions(app, request)(scope, receive, send)
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/_exception_handler.py", line 62, in wrapped_app
|
||||
stderr: raise exc
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/_exception_handler.py", line 51, in wrapped_app
|
||||
stderr: await app(scope, receive, sender)
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/routing.py", line 73, in app
|
||||
stderr: response = await f(request)
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/fastapi/routing.py", line 301, in app
|
||||
stderr: raw_response = await run_endpoint_function(
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/fastapi/routing.py", line 212, in run_endpoint_function
|
||||
stderr: return await dependant.call(**values)
|
||||
stderr: File "/app/xiaomusic/httpserver.py", line 124, in getvolume
|
||||
stderr: volume = await xiaomusic.get_volume(did=did)
|
||||
stderr: File "/app/xiaomusic/xiaomusic.py", line 809, in get_volume
|
||||
stderr: return await self.devices[did].get_volume()
|
||||
stderr: File "/app/xiaomusic/xiaomusic.py", line 1400, in get_volume
|
||||
stderr: playing_info = await self.xiaomusic.mina_service.player_get_status(
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/miservice/minaservice.py", line 103, in player_get_status
|
||||
stderr: return await self.ubus_request(
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/miservice/minaservice.py", line 59, in ubus_request
|
||||
stderr: result = await self.mina_request(
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/miservice/minaservice.py", line 49, in mina_request
|
||||
stderr: return await self.account.mi_request(
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/miservice/miaccount.py", line 150, in mi_request
|
||||
stderr: raise Exception(f"Error {url}: {resp}")
|
||||
stderr: Exception: Error https://api2.mina.mi.com/remote/ubus: {'code': 101, 'message': 'ubus server or device returned invalid result', 'data': {'device_data': '{"msg":"Device is offline","code":608}', 'reqID': 'app_ios_YfG6nlPVAtwDHEgMeJXxcOiZQLrR57'}}
|
||||
stderr: [08:58:05] [0.3.37] [ERROR] h11_impl.py:411: Exception in ASGI application
|
||||
stderr: Traceback (most recent call last):
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/uvicorn/protocols/http/h11_impl.py", line 406, in run_asgi
|
||||
stderr: result = await app( # type: ignore[func-returns-value]
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/uvicorn/middleware/proxy_headers.py", line 70, in __call__
|
||||
stderr: return await self.app(scope, receive, send)
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/fastapi/applications.py", line 1054, in __call__
|
||||
stderr: await super().__call__(scope, receive, send)
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/applications.py", line 113, in __call__
|
||||
stderr: await self.middleware_stack(scope, receive, send)
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/middleware/errors.py", line 187, in __call__
|
||||
stderr: raise exc
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/middleware/errors.py", line 165, in __call__
|
||||
stderr: await self.app(scope, receive, _send)
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/middleware/cors.py", line 85, in __call__
|
||||
stderr: await self.app(scope, receive, send)
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 62, in __call__
|
||||
stderr: await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/_exception_handler.py", line 62, in wrapped_app
|
||||
stderr: raise exc
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/_exception_handler.py", line 51, in wrapped_app
|
||||
stderr: await app(scope, receive, sender)
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/routing.py", line 715, in __call__
|
||||
stderr: await self.middleware_stack(scope, receive, send)
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/routing.py", line 735, in app
|
||||
stderr: await route.handle(scope, receive, send)
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/routing.py", line 288, in handle
|
||||
stderr: await self.app(scope, receive, send)
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/routing.py", line 76, in app
|
||||
stderr: await wrap_app_handling_exceptions(app, request)(scope, receive, send)
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/_exception_handler.py", line 62, in wrapped_app
|
||||
stderr: raise exc
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/_exception_handler.py", line 51, in wrapped_app
|
||||
stderr: await app(scope, receive, sender)
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/starlette/routing.py", line 73, in app
|
||||
stderr: response = await f(request)
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/fastapi/routing.py", line 301, in app
|
||||
stderr: raw_response = await run_endpoint_function(
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/fastapi/routing.py", line 212, in run_endpoint_function
|
||||
stderr: return await dependant.call(**values)
|
||||
stderr: File "/app/xiaomusic/httpserver.py", line 124, in getvolume
|
||||
stderr: volume = await xiaomusic.get_volume(did=did)
|
||||
stderr: File "/app/xiaomusic/xiaomusic.py", line 809, in get_volume
|
||||
stderr: return await self.devices[did].get_volume()
|
||||
stderr: File "/app/xiaomusic/xiaomusic.py", line 1400, in get_volume
|
||||
stderr: playing_info = await self.xiaomusic.mina_service.player_get_status(
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/miservice/minaservice.py", line 103, in player_get_status
|
||||
stderr: return await self.ubus_request(
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/miservice/minaservice.py", line 59, in ubus_request
|
||||
stderr: result = await self.mina_request(
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/miservice/minaservice.py", line 49, in mina_request
|
||||
stderr: return await self.account.mi_request(
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/miservice/miaccount.py", line 150, in mi_request
|
||||
stderr: raise Exception(f"Error {url}: {resp}")
|
||||
stderr: Exception: Error https://api2.mina.mi.com/remote/ubus: {'code': 101, 'message': 'ubus server or device returned invalid result', 'data': {'device_data': '{"msg":"Device is offline","code":608}', 'reqID': 'app_ios_YfG6nlPVAtwDHEgMeJXxcOiZQLrR57'}}
|
||||
stdout: [08:58:05] [0.3.37] [INFO] 10.0.80.191:24020 - "GET /playingmusic?did=566731712 HTTP/1.1" 200
|
||||
stdout: [08:58:05] [0.3.37] [INFO] 10.0.80.191:24021 - "GET /playingmusic?did=566731712 HTTP/1.1" 200
|
||||
stdout: [08:58:07] [0.3.37] [INFO] 10.0.80.191:24021 - "GET /playingmusic?did=566731712 HTTP/1.1" 200
|
||||
stdout: [08:58:08] [0.3.37] [INFO] 10.0.80.191:24021 - "GET /playingmusic?did=566731712 HTTP/1.1" 200
|
||||
stdout: [08:58:08] [0.3.37] [INFO] 10.0.80.191:24020 - "GET /playingmusic?did=566731712 HTTP/1.1" 200
|
||||
stderr: [08:58:09] [0.3.37] [INFO] httpserver.py:177: docmd. did:566731712 cmd:播放列表m3u电台|80后音悦台
|
||||
stderr: [08:58:09] [0.3.37] [INFO] xiaomusic.py:582: cancel_all_tasks no task
|
||||
stdout: [08:58:09] [0.3.37] [INFO] 10.0.80.191:24020 - "POST /cmd HTTP/1.1" 200
|
||||
stderr: [08:58:09] [0.3.37] [INFO] xiaomusic.py:560: 收到消息:播放列表m3u电台|80后音悦台 控制面板:True did:566731712
|
||||
stderr: [08:58:09] [0.3.37] [INFO] xiaomusic.py:648: 匹配到指令. opkey:播放列表 opvalue:play_music_list oparg:m3u电台|80后音悦台
|
||||
stderr: [08:58:09] [0.3.37] [INFO] xiaomusic.py:716: 根据【m3u电台】找到播放列表【m3u电台】
|
||||
stderr: [08:58:09] [0.3.37] [INFO] xiaomusic.py:991: 没打乱 m3u电台 ['80后音悦台', 'BBC Radio 1', 'BBC Radio 2', 'BBC Radio 3', 'BBC Radio 4', 'CNA938', 'FM 988', ' AFO Radio', 'BBC News', 'BBC Radio 1 Dance', 'BBC Radio 1 Relax', 'BBC Radio 1Xtra', 'BBC Radio 4 Extra', 'BBC Radio 5 Live', 'BBC Radio 6 Music', 'BCC中广新闻', 'BCC中广流行', 'BCC中广音乐', 'CNA亚洲新闻', 'CNR中国之声', 'CNR乡村之声', 'CNR交通广播', 'CNR台海之声', 'CNR文艺之声', 'CNR湾区之声', 'CNR神州之声', 'CNR经典音乐', 'CNR经济之声', 'CNR老年之声', 'CNR阅读之声', 'CNR音乐之声', 'CRI世界华声', 'CRI华语环球', 'CRI南海之声', 'CRI环球资讯', 'CRI英语资讯', 'CRI轻松调频', 'Capital FM', 'CityFM台之音', 'Cool Radio', 'Gold FM', 'Hit FM劲曲', 'Kiss FM', 'LBC News', 'LBC UK', 'Love FM', 'Love Radio', 'Money FM', 'NPR News', 'News Radio UK', 'One FM', 'Power FM', 'RFA', 'RFI', 'RTI中央广播', 'Times Radio', 'VOA环球英语', 'Yes FM', '上海故事广播', '上海流行音乐', '上海音乐广播', '上海音乐电台', '中文流行', '全国广播', '北京交通广播', '北京体育广播', '北京城市广播', '北京文艺广播', '北京新闻广播', '北京经典调频', '北京音乐广播', '南京音乐广播', '台中广播电台', '四川文艺广播', '国乐悠扬', '天籁之音 Hi-Fi', '天籁古典', '天籁国风', '安全百科', '山西文艺广播', '广东音乐之声', '广州汽车音乐', '摇滚天空', '有声文摘', '民谣蓝调', '民谣音乐台', '江苏故事广播', '江苏文艺广播', '江苏音乐广播', '河北文艺广播', '河北汽车音乐', '深圳音乐广播', '湖南文艺广播', '湖南旅游广播', '湖南音乐之声', '潮流音悦台', '经典FM', '美国之音', '陕西故事广播', '陕西音乐广播']
|
||||
stderr: [08:58:09] [0.3.37] [INFO] xiaomusic.py:1422: 开始播放列表m3u电台
|
||||
stderr: [08:58:09] [0.3.37] [INFO] xiaomusic.py:1005: play. search_key: name:80后音悦台
|
||||
stderr: [08:58:09] [0.3.37] [INFO] xiaomusic.py:663: 根据【80后音悦台】找到歌曲【80后音悦台】
|
||||
stderr: [08:58:09] [0.3.37] [INFO] xiaomusic.py:1088: cur_music 80后音悦台
|
||||
stderr: [08:58:09] [0.3.37] [INFO] xiaomusic.py:388: get_music_url web music. name:80后音悦台, url:http://stream3.hndt.com/now/SFZeH2cb/playlist.m3u8
|
||||
stderr: [08:58:09] [0.3.37] [INFO] xiaomusic.py:360: get_music_sec_url. name:80后音悦台 url:http://stream3.hndt.com/now/SFZeH2cb/playlist.m3u8
|
||||
stderr: [08:58:09] [0.3.37] [INFO] xiaomusic.py:362: 电台不会有播放时长
|
||||
stderr: [08:58:09] [0.3.37] [INFO] xiaomusic.py:1437: group_force_stop_xiaoai ['be1daeaa-df03-4a27-8aff-404356abfa9a']
|
||||
stderr: [08:58:09] [0.3.37] [ERROR] xiaomusic.py:1137: Execption Error https://api2.mina.mi.com/remote/ubus: {'code': 101, 'message': 'ubus server or device returned invalid result', 'data': {'device_data': '{"msg":"Device is offline","code":608}', 'reqID': 'app_ios_10DxZOintrGWhyETo4q9auU3VMNH6A'}}
|
||||
stderr: Traceback (most recent call last):
|
||||
stderr: File "/app/xiaomusic/xiaomusic.py", line 1131, in force_stop_xiaoai
|
||||
stderr: ret = await self.xiaomusic.mina_service.player_pause(device_id)
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/miservice/minaservice.py", line 79, in player_pause
|
||||
stderr: return await self.ubus_request(
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/miservice/minaservice.py", line 59, in ubus_request
|
||||
stderr: result = await self.mina_request(
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/miservice/minaservice.py", line 49, in mina_request
|
||||
stderr: return await self.account.mi_request(
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/miservice/miaccount.py", line 150, in mi_request
|
||||
stderr: raise Exception(f"Error {url}: {resp}")
|
||||
stderr: Exception: Error https://api2.mina.mi.com/remote/ubus: {'code': 101, 'message': 'ubus server or device returned invalid result', 'data': {'device_data': '{"msg":"Device is offline","code":608}', 'reqID': 'app_ios_10DxZOintrGWhyETo4q9auU3VMNH6A'}}
|
||||
stderr: [08:58:09] [0.3.37] [INFO] xiaomusic.py:1440: group_force_stop_xiaoai ['be1daeaa-df03-4a27-8aff-404356abfa9a'] [None]
|
||||
stderr: [08:58:09] [0.3.37] [INFO] xiaomusic.py:1091: 播放 http://stream3.hndt.com/now/SFZeH2cb/playlist.m3u8
|
||||
stderr: [08:58:10] [0.3.37] [ERROR] xiaomusic.py:1332: Execption Error https://api2.mina.mi.com/remote/ubus: {'code': 101, 'message': 'ubus server or device returned invalid result', 'data': {'device_data': '{"msg":"Device is offline","code":608}', 'reqID': 'app_ios_Bbkf4HLQiN7tjl1rGD2gpXuJ8yI0nP'}}
|
||||
stderr: Traceback (most recent call last):
|
||||
stderr: File "/app/xiaomusic/xiaomusic.py", line 1327, in play_one_url
|
||||
stderr: ret = await self.xiaomusic.mina_service.play_by_url(device_id, url)
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/miservice/minaservice.py", line 125, in play_by_url
|
||||
stderr: return await self.ubus_request(
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/miservice/minaservice.py", line 59, in ubus_request
|
||||
stderr: result = await self.mina_request(
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/miservice/minaservice.py", line 49, in mina_request
|
||||
stderr: return await self.account.mi_request(
|
||||
stderr: File "/app/.venv/lib/python3.10/site-packages/miservice/miaccount.py", line 150, in mi_request
|
||||
stderr: raise Exception(f"Error {url}: {resp}")
|
||||
stderr: Exception: Error https://api2.mina.mi.com/remote/ubus: {'code': 101, 'message': 'ubus server or device returned invalid result', 'data': {'device_data': '{"msg":"Device is offline","code":608}', 'reqID': 'app_ios_Bbkf4HLQiN7tjl1rGD2gpXuJ8yI0nP'}}
|
||||
stderr: [08:58:10] [0.3.37] [INFO] xiaomusic.py:1305: group_player_play http://stream3.hndt.com/now/SFZeH2cb/playlist.m3u8 ['be1daeaa-df03-4a27-8aff-404356abfa9a'] [None]
|
||||
stderr: [08:58:10] [0.3.37] [INFO] xiaomusic.py:1094: 播放 80后音悦台 失败
|
||||
|
||||
---
|
||||
|
||||
### 评论 4 - hanxi
|
||||
|
||||
设备掉线了
|
||||
|
||||
---
|
||||
|
||||
### 评论 5 - 201692929
|
||||
|
||||
怎么获取 他正在播放什么?或者是播放进度 ?播放列表?我想给他加进去
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
### 评论 6 - hanxi
|
||||
|
||||
> 怎么获取 他正在播放什么?或者是播放进度 ?播放列表?我想给他加进去 
|
||||
|
||||
这个接口 `/playingmusic`
|
||||
|
||||
---
|
||||
|
||||
### 评论 7 - 114514thD
|
||||
|
||||
加不加"type":"radio"都会一直播放不切换到下一首歌是为什么呢
|
||||
|
||||
---
|
||||
|
||||
### 评论 8 - hanxi
|
||||
|
||||
> 加不加"type":"radio"都会一直播放不切换到下一首歌是为什么呢
|
||||
|
||||
发出来看看?
|
||||
|
||||
---
|
||||
|
||||
### 评论 9 - 114514thD
|
||||
|
||||
> > 加不加"type":"radio"都会一直播放不切换到下一首歌是为什么呢
|
||||
>
|
||||
> 发出来看看?
|
||||
|
||||
~~本地开服务器,生成的m3u列表
|
||||
格式如下
|
||||
`#EXTINF:247,周传雄 - 临别一眼.mp3
|
||||
http://192.168.1.147:8000/%E5%91%A8%E4%BC%A0%E9%9B%84%20-%20%E4%B8%B4%E5%88%AB%E4%B8%80%E7%9C%BC.mp3`
|
||||
包含了时长信息
|
||||
版本是0.3.46
|
||||
potplayer里播放完全正常~~
|
||||
|
||||
仔细研究了一下,发现确实存在问题,不过是另一种情况,下面单说
|
||||
|
||||
---
|
||||
|
||||
### 评论 10 - 114514thD
|
||||
|
||||
> > 加不加"type":"radio"都会一直播放不切换到下一首歌是为什么呢
|
||||
>
|
||||
> 发出来看看?
|
||||
|
||||
经过实验发现,本地生成的m3u用potplayer播放正常
|
||||

|
||||
转换为json(去掉"type":"radio")后用小爱播放也正常
|
||||

|
||||
|
||||
但是alist链接就不正常,alist生成的m3u格式如下
|
||||
`#EXTM3U
|
||||
#EXTINF:-1,Let Me Hear.mp3
|
||||
http://192.168.1.198:5244/d/%E7%BD%91%E6%98%93%E4%BA%91%E9%9F%B3%E4%B9%90%20%E9%9F%B3%E4%B9%90%E4%BA%91%E7%9B%98/Let%20Me%20Hear.mp3?sign=xxxx=:0`
|
||||
没有时长信息,但是用potplayer一播放就出现时长了
|
||||

|
||||
而用小爱播放就始终没有时长(切歌、等待都试过了)
|
||||

|
||||
大佬你的示例链接(gist.github.com/hanxi/dda82d964a28f8110f8fba81c3ff8314)里的又是正常的,感觉可能是alist的流比较特殊。。
|
||||

|
||||
|
||||
---
|
||||
|
||||
### 评论 11 - 114514thD
|
||||
|
||||
> > 加不加"type":"radio"都会一直播放不切换到下一首歌是为什么呢
|
||||
>
|
||||
> 发出来看看?
|
||||
|
||||
这几天再仔细研究了一下,发现一个可能的原因:这样获取到的是m4a文件,我尝试着在json里配置获取到的m4a链接,发现播放同样也是无时长
|
||||
|
||||
---
|
||||
|
||||
### 评论 12 - hanxi
|
||||
|
||||
获取歌曲时长确实有些格式获取不到。
|
||||
|
||||
---
|
||||
|
||||
### 评论 13 - 114514thD
|
||||
|
||||
> 获取歌曲时长确实有些格式获取不到。
|
||||
|
||||
http://m7.music.126.net/20241216093525/75c9080afa2929d7eec8e1cdbcbc0a92/yyaac/0709/535a/5358/0c6e2dcac3d0e9fa4415d22e1eca1337.m4a
|
||||
以这个文件为例,我用ffmpeg可以获取时长等元数据
|
||||
`
|
||||
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'http://m7.music.126.net/20241216093525/75c9080afa2929d7eec8e1cdbcbc0a92/yyaac/0709/535a/5358/0c6e2dcac3d0e9fa4415d22e1eca1337.m4a':
|
||||
Metadata:
|
||||
major_brand : mp42
|
||||
minor_version : 0
|
||||
compatible_brands: M4A mp42isom
|
||||
creation_time : 2019-02-21T02:51:36.000000Z
|
||||
iTunSMPB : 00000000 00000920 000003E8 00000000004BE2F8 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
|
||||
encoder : Nero AAC codec / 1.5.4.0
|
||||
Duration: 00:03:45.70, start: 0.052971, bitrate: 64 kb/s
|
||||
Chapters:
|
||||
Chapter #0:0: start 0.105941, end 225.750930
|
||||
Metadata:
|
||||
title :
|
||||
Stream #0:0[0x1](und): Audio: aac (HE-AAC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 63 kb/s (default)
|
||||
Metadata:
|
||||
creation_time : 2019-02-21T02:51:36.000000Z
|
||||
handler_name : Sound Media Handler
|
||||
vendor_id : [0][0][0][0]`
|
||||
那是为什么播放就不行呢
|
||||
|
||||
---
|
||||
|
||||
### 评论 14 - hanxi
|
||||
|
||||
因为代码有问题。
|
||||
|
||||
---
|
||||
|
||||
### 评论 15 - 114514thD
|
||||
|
||||
> 因为代码有问题。
|
||||
|
||||
好吧😂😂,大佬真是直接😁
|
||||
|
||||
---
|
||||
|
||||
### 评论 16 - hanxi
|
||||
|
||||
重构方案 #314
|
||||
|
||||
---
|
||||
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/78)
|
||||
31
docs/issues/86.md
Normal file
31
docs/issues/86.md
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
title: 微信交流群二维码
|
||||
---
|
||||
|
||||
# 微信交流群二维码
|
||||
|
||||

|
||||
|
||||
|
||||
## 评论
|
||||
|
||||
|
||||
### 评论 1 - DarrenWen
|
||||
|
||||
群满200人,没法加入了
|
||||
|
||||
---
|
||||
|
||||
### 评论 2 - hanxi
|
||||
|
||||
没想到这么快就满了,新建了一个。
|
||||
|
||||
---
|
||||
|
||||
### 评论 3 - hanxi
|
||||
|
||||

|
||||
|
||||
|
||||
---
|
||||
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/86)
|
||||
74
docs/issues/88.md
Normal file
74
docs/issues/88.md
Normal file
@@ -0,0 +1,74 @@
|
||||
---
|
||||
title: 如何添加m3u格式文件的电台
|
||||
---
|
||||
|
||||
# 如何添加m3u格式文件的电台
|
||||
|
||||
比如可以找到这样的 m3u 电台文件: https://github.com/YueChan/Live/blob/main/Radio.m3u
|
||||
|
||||
1. 复制文件内容,粘贴到 m3u 转换工具里,点击转换为 json 格式:
|
||||
|
||||

|
||||
|
||||
2. 然后复制 json 内容,粘贴到歌单内容里,点击保存,再返回首页:
|
||||
|
||||

|
||||
|
||||
3. 在首页点击刷新列表,选择所有电台,再点击播放列表歌曲:
|
||||
|
||||

|
||||
|
||||
4. 也可以用口令播放电台: `播放列表所有电台` ,或者口令: `播放歌曲北京城市广播`
|
||||
|
||||
## 评论
|
||||
|
||||
|
||||
### 评论 1 - guoxiangke
|
||||
|
||||
转换m3u链接: http://127.0.0.1:8090/static/m3u.html
|
||||
|
||||
---
|
||||
|
||||
### 评论 2 - guoxiangke
|
||||
|
||||
http://127.0.0.1:8090/playurl?did=mydid&url=https://a.com/test.m3u 如果能支持 这么播放m3u 就太完美了
|
||||
希望能够支持,谢谢作者。
|
||||
|
||||
---
|
||||
|
||||
### 评论 3 - hanxi
|
||||
|
||||
> http://127.0.0.1:8090/playurl?did=mydid&url=https://a.com/test.m3u 如果能支持 这么播放m3u 就太完美了 希望能够支持,谢谢作者。
|
||||
|
||||
不想破坏现有接口,可以考虑用插件的方式来实现。
|
||||
|
||||
---
|
||||
|
||||
### 评论 4 - lazybabyz
|
||||
|
||||
potplayer 测试 https://raw.githubusercontent.com/YueChan/Live/refs/heads/main/Radio.m3u 部分可以听
|
||||
xiaomusic测试 https://github.com/YueChan/Live/blob/main/Radio.m3u 复制raw文件转换 全部失败不停转台
|
||||
|
||||
以下potplayer测试 可以听,xiaomusic测试 复制raw文件转换 全部失败不停转台
|
||||
|
||||
https://raw.githubusercontent.com/kaige-cai/live/refs/heads/main/radio.m3u
|
||||
https://raw.githubusercontent.com/imDazui/Tvlist-awesome-m3u-m3u8/master/m3u/%E5%B9%BF%E6%92%AD%E7%94%B5%E5%8F%B02021.m3u
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 评论 5 - hanxi
|
||||
|
||||
检查一下是不是 ipv6 的地址?小米音箱不支持 ipv6
|
||||
|
||||
---
|
||||
|
||||
### 评论 6 - lazybabyz
|
||||
|
||||
> 检查一下是不是 ipv6 的地址?小米音箱不支持 ipv6
|
||||
|
||||
是我硬件设置的问题 重新安装了 解决!
|
||||
|
||||
---
|
||||
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/88)
|
||||
101
docs/issues/94.md
Normal file
101
docs/issues/94.md
Normal file
@@ -0,0 +1,101 @@
|
||||
---
|
||||
title: 采用config.json配置方式
|
||||
---
|
||||
|
||||
# 采用config.json配置方式
|
||||
|
||||
docker 方式部署默认推荐使用环境变量的方式来配置参数,如果是自己用命令行启动,目前支持的参数配置比较少,但是是支持 `--config` 参数。
|
||||
|
||||
使用 pip 安装 xiaomusic 【0.1.83版本才支持 pip 安装】
|
||||
|
||||
```shell
|
||||
pip install xiaomusic
|
||||
```
|
||||
|
||||
依赖的 ffmpeg 需要自己安装。
|
||||
|
||||

|
||||
|
||||
仓库中有个 config-example.json 文件,可以把这个文件拷贝为 config.json 然后修改 config.json 里的配置,再用下面的命令启动。
|
||||
|
||||
```shell
|
||||
xiaomusic --config ./config.json
|
||||
```
|
||||
|
||||
默认的 config.json 模板如下:
|
||||
|
||||
```json
|
||||
{
|
||||
"hardware": "L07A",
|
||||
"account": "",
|
||||
"password": "",
|
||||
"mi_did": "",
|
||||
"cookie": "",
|
||||
"verbose": false,
|
||||
"music_path": "music",
|
||||
"conf_path": null,
|
||||
"hostname": "192.168.2.5",
|
||||
"port": 8090,
|
||||
"proxy": null,
|
||||
"search_prefix": "ytsearch:",
|
||||
"ffmpeg_location": "./ffmpeg/bin",
|
||||
"active_cmd": "play,random_play,playlocal,play_music_list,stop",
|
||||
"exclude_dirs": "@eaDir",
|
||||
"music_path_depth": 10,
|
||||
"disable_httpauth": true,
|
||||
"httpauth_username": "admin",
|
||||
"httpauth_password": "admin",
|
||||
"music_list_url": "",
|
||||
"music_list_json": "",
|
||||
"disable_download": false,
|
||||
"use_music_api": false,
|
||||
"log_file": "/tmp/xiaomusic.txt",
|
||||
"fuzzy_match_cutoff": 0.6,
|
||||
"enable_fuzzy_match": true
|
||||
}
|
||||
```
|
||||
|
||||
如果采用 docker compose 启动想用 config.json 的配置方式,可以这样配:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
xiaomusic:
|
||||
image: hanxi/xiaomusic
|
||||
container_name: xiaomusic
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 8090:8090
|
||||
volumes:
|
||||
- ./music:/app/music
|
||||
- ./config.json:/app/config.json
|
||||
command: ['--config', '/app/config.json']
|
||||
```
|
||||
主要就是把 config.json 文件映射进容器和传 `--config` 参数。
|
||||
|
||||
## 评论
|
||||
|
||||
|
||||
### 评论 1 - alitime
|
||||
|
||||
正需要,有配置文件方便多了
|
||||
|
||||
---
|
||||
|
||||
### 评论 2 - hanxi
|
||||
|
||||
ffmpeg 如果是用 apt install 这类系统工具安装的,默认会在 /usr/bin 目录下,ffmpeg_location 这个参数就需要设置为 /usr/bin .
|
||||
|
||||
目前 armv7 cpu 的 docker 镜像里的 ffmpeg 有问题,最好是用 pip 方式安装运行。
|
||||
|
||||
---
|
||||
|
||||
### 评论 3 - hanxi
|
||||
|
||||
> ffmpeg 如果是用 apt install 这类系统工具安装的,默认会在 /usr/bin 目录下,ffmpeg_location 这个参数就需要设置为 /usr/bin .
|
||||
>
|
||||
> 目前 armv7 cpu 的 docker 镜像里的 ffmpeg 有问题,最好是用 pip 方式安装运行。
|
||||
|
||||
armv7 问题已经解决。
|
||||
|
||||
---
|
||||
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/94)
|
||||
17
docs/issues/96.md
Normal file
17
docs/issues/96.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: ios系统上的捷径配置
|
||||
---
|
||||
|
||||
# ios系统上的捷径配置
|
||||
|
||||
下面是播放音乐和关机两个示例。只要在 web 页面上能看到的功能,都有对应的 http 请求接口,都可以用来配置捷径。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
## 评论
|
||||
|
||||
没有评论。
|
||||
[链接到 GitHub Issue](https://github.com/hanxi/xiaomusic/issues/96)
|
||||
1360
docs/issues/99.md
Normal file
1360
docs/issues/99.md
Normal file
File diff suppressed because it is too large
Load Diff
1089
docs/issues/changelog.md
Normal file
1089
docs/issues/changelog.md
Normal file
File diff suppressed because it is too large
Load Diff
296
docs/issues/index.md
Normal file
296
docs/issues/index.md
Normal file
@@ -0,0 +1,296 @@
|
||||
# XiaoMusic: 无限听歌,解放小爱音箱
|
||||
|
||||
[](https://github.com/hanxi/xiaomusic)
|
||||
[](https://hub.docker.com/r/hanxi/xiaomusic)
|
||||
[](https://hub.docker.com/r/hanxi/xiaomusic)
|
||||
[](https://pypi.org/project/xiaomusic/)
|
||||
[](https://pypi.org/project/xiaomusic/)
|
||||
[](https://pypi.org/project/xiaomusic/)
|
||||
[](https://github.com/hanxi/xiaomusic/releases)
|
||||
[](https://visitorbadge.io/status?path=hanxi%2Fxiaomusic)
|
||||
[](https://visitorbadge.io/status?path=hanxi%2Fxiaomusic)
|
||||
|
||||
使用小爱音箱播放音乐,音乐使用 yt-dlp 下载。
|
||||
|
||||
<https://github.com/hanxi/xiaomusic>
|
||||
|
||||
> [!TIP]
|
||||
> 初次安装遇到问题请查阅 [💬 FAQ问题集合](https://github.com/hanxi/xiaomusic/issues/99) ,一般遇到的问题都已经有解决办法。
|
||||
|
||||
## 👋 最简配置运行
|
||||
|
||||
已经支持在 web 页面配置其他参数,docker 启动命令如下:
|
||||
|
||||
```bash
|
||||
docker run -p 58090:8090 -e XIAOMUSIC_PUBLIC_PORT=58090 -v /xiaomusic_music:/app/music -v /xiaomusic_conf:/app/conf hanxi/xiaomusic
|
||||
```
|
||||
|
||||
🔥 国内:
|
||||
|
||||
```bash
|
||||
docker run -p 58090:8090 -e XIAOMUSIC_PUBLIC_PORT=58090 -v /xiaomusic_music:/app/music -v /xiaomusic_conf:/app/conf m.daocloud.io/docker.io/hanxi/xiaomusic
|
||||
```
|
||||
|
||||
对应的 docker compose 配置如下:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
xiaomusic:
|
||||
image: hanxi/xiaomusic
|
||||
container_name: xiaomusic
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 58090:8090
|
||||
environment:
|
||||
XIAOMUSIC_PUBLIC_PORT: 58090
|
||||
volumes:
|
||||
- /xiaomusic_music:/app/music
|
||||
- /xiaomusic_conf:/app/conf
|
||||
```
|
||||
|
||||
🔥 国内:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
xiaomusic:
|
||||
image: m.daocloud.io/docker.io/hanxi/xiaomusic
|
||||
container_name: xiaomusic
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 58090:8090
|
||||
environment:
|
||||
XIAOMUSIC_PUBLIC_PORT: 58090
|
||||
volumes:
|
||||
- /xiaomusic_music:/app/music
|
||||
- /xiaomusic_conf:/app/conf
|
||||
```
|
||||
|
||||
- 其中 conf 目录为配置文件存放目录,music 目录为音乐存放目录,建议分开配置为不同的目录。
|
||||
- /xiaomusic_music 和 /xiaomusic_conf 是 docker 主机里的目录,可以修改为其他目录。如果报错找不到 /xiaomusic_music 目录,可以先执行 `mkdir -p /xiaomusic_{music,conf}` 命令新建目录。
|
||||
- XIAOMUSIC_PUBLIC_PORT 是用来配置 NAS 本地端口的。8090 是容器端口,不要去修改。
|
||||
- 后台访问地址为: http://NAS_IP:58090
|
||||
|
||||
> [!NOTE]
|
||||
> docker 和 docker compose 二选一即可,启动成功后,在 web 页面可以配置其他参数,带有 `*` 号的配置是必须要配置的,其他的用不上时不用修改。初次配置时需要在页面上输入小米账号和密码保存后才能获取到设备列表。
|
||||
|
||||
> [!TIP]
|
||||
> 目前安装步骤已经是最简化了,如果还是嫌安装麻烦,可以微信或者 QQ 约我远程安装,我一般周末和晚上才有时间,收个辛苦费 :moneybag: 50 元一次,安装失败不收费。
|
||||
|
||||
遇到问题可以去 web 设置页面底部点击【下载日志文件】按钮,然后搜索一下日志文件内容确保里面没有账号密码信息后(有就删除这些敏感信息),然后在提 issues 反馈问题时把下载的日志文件带上。
|
||||
|
||||
> [!TIP]
|
||||
> 海外 RackNerd VPS 机器推荐,可支付宝付款。
|
||||
> - [🔥1 GB KVM VPS $11.29/年](https://my.racknerd.com/aff.php?aff=1177&pid=903)
|
||||
> - [2 GB KVM VPS](https://my.racknerd.com/aff.php?aff=1177&pid=904)
|
||||
> - [3.5 GB KVM VPS](https://my.racknerd.com/aff.php?aff=1177&pid=905)
|
||||
> - [4 GB KVM VPS](https://my.racknerd.com/aff.php?aff=1177&pid=906)
|
||||
> - [6 GB KVM VPS](https://my.racknerd.com/aff.php?aff=1177&pid=907)
|
||||
|
||||
### 🤐 支持语音口令
|
||||
|
||||
- 【播放歌曲】,播放本地的歌曲
|
||||
- 【播放歌曲+歌名】,比如:播放歌曲周杰伦晴天
|
||||
- 【上一首】
|
||||
- 【下一首】
|
||||
- 【单曲循环】
|
||||
- 【全部循环】
|
||||
- 【随机播放】
|
||||
- 【关机】,【停止播放】,两个效果是一样的。
|
||||
- 【刷新列表】,当复制了歌曲进 music 目录后,可以用这个口令刷新歌单。
|
||||
- 【播放列表+列表名】,比如:播放列表其他。
|
||||
- 【加入收藏】,把当前播放的歌曲加入收藏歌单。
|
||||
- 【取消收藏】,把当前播放的歌曲从收藏歌单里移除。
|
||||
- 【播放列表收藏】,这个用于播放收藏歌单。
|
||||
- 【播放本地歌曲+歌名】,这个口令和播放歌曲的区别是本地找不到也不会去下载。
|
||||
- 【播放列表第几个+列表名】,具体见: <https://github.com/hanxi/xiaomusic/issues/158>
|
||||
- 【搜索播放+关键词】,会搜索关键词作为临时搜索列表播放,比如说【搜索播放林俊杰】,会播放所有林俊杰的歌。
|
||||
- 【本地搜索播放+关键词】,跟搜索播放的区别是本地找不到也不会去下载。
|
||||
|
||||
> [!TIP]
|
||||
> 隐藏玩法: 对小爱同学说播放歌曲小猪佩奇的故事,会先下载小猪佩奇的故事,然后再播放小猪佩奇的故事。
|
||||
|
||||
## 🛠️ pip 方式安装运行
|
||||
|
||||
```shell
|
||||
> pip install -U xiaomusic
|
||||
> xiaomusic --help
|
||||
__ __ _ __ __ _
|
||||
\ \/ / (_) __ _ ___ | \/ | _ _ ___ (_) ___
|
||||
\ / | | / _` | / _ \ | |\/| | | | | | / __| | | / __|
|
||||
/ \ | | | (_| | | (_) | | | | | | |_| | \__ \ | | | (__
|
||||
/_/\_\ |_| \__,_| \___/ |_| |_| \__,_| |___/ |_| \___|
|
||||
XiaoMusic v0.3.65 by: github.com/hanxi
|
||||
|
||||
usage: xiaomusic [-h] [--port PORT] [--hardware HARDWARE] [--account ACCOUNT]
|
||||
[--password PASSWORD] [--cookie COOKIE] [--verbose]
|
||||
[--config CONFIG] [--ffmpeg_location FFMPEG_LOCATION]
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--port PORT 监听端口
|
||||
--hardware HARDWARE 小爱音箱型号
|
||||
--account ACCOUNT xiaomi account
|
||||
--password PASSWORD xiaomi password
|
||||
--cookie COOKIE xiaomi cookie
|
||||
--verbose show info
|
||||
--config CONFIG config file path
|
||||
--ffmpeg_location FFMPEG_LOCATION
|
||||
ffmpeg bin path
|
||||
> xiaomusic --config config.json
|
||||
```
|
||||
|
||||
其中 `config.json` 文件可以参考 `config-example.json` 文件配置。见 <https://github.com/hanxi/xiaomusic/issues/94>
|
||||
|
||||
不修改默认端口 8090 的情况下,只需要执行 `xiaomusic` 即可启动。
|
||||
|
||||
## 🔩 开发环境运行
|
||||
|
||||
- 使用 install_dependencies.sh 下载依赖
|
||||
- 使用 pdm 安装环境
|
||||
- 默认监听了端口 8090 , 使用其他端口自行修改。
|
||||
|
||||
```shell
|
||||
pdm run xiaomusic.py
|
||||
````
|
||||
|
||||
如果是开发前端界面,可以通过 <http://localhost:8090/docs>
|
||||
查看有什么接口。目前的 web 控制台非常简陋,欢迎有兴趣的朋友帮忙实现一个漂亮的前端,需要什么接口可以随时提需求。
|
||||
|
||||
### 🚦 代码提交规范
|
||||
|
||||
提交前请执行
|
||||
|
||||
```
|
||||
pdm lintfmt
|
||||
```
|
||||
|
||||
用于检查代码和格式化代码。
|
||||
|
||||
### 本地编译 Docker Image
|
||||
|
||||
```shell
|
||||
docker build -t xiaomusic .
|
||||
```
|
||||
|
||||
### 技术栈
|
||||
|
||||
- 后端代码使用 Python 语言编写。
|
||||
- HTTP 服务使用的是 FastAPI 框架,~~早期版本使用的是 Flask~~。
|
||||
- 使用了 Docker ,在 NAS 上安装更方便。
|
||||
- 默认的前端主题使用了 jQuery 。
|
||||
|
||||
## 已测试支持的设备
|
||||
|
||||
| 型号 | 名称 |
|
||||
| ---- | ---------------------------------------------------------------------------------------------- |
|
||||
| L06A | [小爱音箱](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l06a) |
|
||||
| L07A | [Redmi小爱音箱 Play](https://home.mi.com/webapp/content/baike/product/index.html?model=xiaomi.wifispeaker.l7a) |
|
||||
| S12/S12A/MDZ-25-DA | [小米AI音箱](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.s12) |
|
||||
| LX5A | [小爱音箱 万能遥控版](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.lx5a) |
|
||||
| LX05 | [小爱音箱Play(2019款)](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.lx05) |
|
||||
| L15A | [小米AI音箱(第二代)](https://home.mi.com/webapp/content/baike/product/index.html?model=xiaomi.wifispeaker.l15a#/) |
|
||||
| L16A | [Xiaomi Sound](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l16a) |
|
||||
| L17A | [Xiaomi Sound Pro](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l17a) |
|
||||
| LX06 | [小爱音箱Pro](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.lx06) |
|
||||
| LX01 | [小爱音箱mini](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.lx01) |
|
||||
| L05B | [小爱音箱Play](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l05b) |
|
||||
| L05C | [小米小爱音箱Play 增强版](https://home.mi.com/baike/index.html#/detail?model=xiaomi.wifispeaker.l05c) |
|
||||
| L09A | [小米音箱Art](https://home.mi.com/webapp/content/baike/product/index.html?model=xiaomi.wifispeaker.l09a) |
|
||||
| LX04 X10A X08A | 已经支持的触屏版 |
|
||||
| X08C X08E X8F | 需要设置【型号兼容模式】选项为 true |
|
||||
| M01/XMYX01JY | 小米小爱音箱HD 需要设置【特殊型号获取对话记录】选项为 true 才能语音播放|
|
||||
|
||||
型号与产品名称对照可以在这里查询 <https://home.miot-spec.com/s/xiaomi.wifispeaker>
|
||||
|
||||
> [!NOTE]
|
||||
> 如果你的设备支持播放,请反馈给我添加到支持列表里,谢谢。
|
||||
> 目前应该所有设备类型都已经支持播放,有问题随时反馈。
|
||||
> 其他触屏版不能播放可以设置【型号兼容模式】选项为 true 试试。见 <https://github.com/hanxi/xiaomusic/issues/30>
|
||||
|
||||
## 🎵 支持音乐格式
|
||||
|
||||
- mp3
|
||||
- flac
|
||||
- wav
|
||||
- ape
|
||||
- ogg
|
||||
- m4a
|
||||
|
||||
> [!NOTE]
|
||||
> 本地音乐会搜索目录下上面格式的文件,下载的歌曲是 mp3 格式的。
|
||||
> 已知 L05B L05C LX06 L16A 不支持 flac 格式。
|
||||
> 如果格式不能播放可以打开【转换为MP3】和【型号兼容模式】选项。具体见 <https://github.com/hanxi/xiaomusic/issues/153#issuecomment-2328168689>
|
||||
|
||||
## 🌏 网络歌单功能
|
||||
|
||||
可以配置一个 json 格式的歌单,支持电台和歌曲,也可以直接用别人分享的链接,同时配备了 m3u 文件格式转换工具,可以很方便的把 m3u 电台文件转换成网络歌单格式的 json 文件,具体用法见 <https://github.com/hanxi/xiaomusic/issues/78>
|
||||
|
||||
> [!NOTE]
|
||||
> 欢迎有想法的朋友们制作更多的歌单转换工具。
|
||||
|
||||
## 🍺 更多其他可选配置
|
||||
|
||||
见 <https://github.com/hanxi/xiaomusic/issues/333>
|
||||
|
||||
## ⚠️ 安全提醒
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> 1. 如果配置了公网访问 xiaomusic ,请一定要开启密码登陆,并设置复杂的密码。且不要在公共场所的 WiFi 环境下使用,否则可能造成小米账号密码泄露。
|
||||
> 2. 强烈不建议将小爱音箱的小米账号绑定摄像头,代码难免会有 bug ,一旦小米账号密码泄露,可能监控录像也会泄露。
|
||||
|
||||
## 🤔 高级篇
|
||||
|
||||
- 自定义口令功能 <https://github.com/hanxi/xiaomusic/issues/105>
|
||||
- [ ] 缺少一篇教程 [如何写自定义插件](https://github.com/hanxi/xiaomusic/issues/105)
|
||||
|
||||
## 📢 讨论区
|
||||
|
||||
- [点击链接加入QQ频道【xiaomusic】](https://pd.qq.com/s/e2jybz0ss)
|
||||
- [点击链接加入群聊【xiaomusic】 604526973](http://qm.qq.com/cgi-bin/qm/qr?_wv=1027&k=13St5PLVcTxYlWTAs_iAawazjtdD1l-a&authKey=dJWEpaT2fDBDpdUUOWj%2FLt6NS1ePBfShDfz7a6seNURi05VvVnAGQzXF%2FM%2F5HgIm&noverify=0&group_code=604526973)
|
||||
- <https://github.com/hanxi/xiaomusic/issues>
|
||||
- [微信群二维码](https://github.com/hanxi/xiaomusic/issues/86)
|
||||
|
||||
## ❤️ 感谢
|
||||
|
||||
- [xiaomi](https://www.mi.com/)
|
||||
- [PDM](https://pdm.fming.dev/latest/)
|
||||
- [xiaogpt](https://github.com/yihong0618/xiaogpt)
|
||||
- [MiService](https://github.com/yihong0618/MiService)
|
||||
- [实现原理](https://github.com/yihong0618/gitblog/issues/258)
|
||||
- [yt-dlp](https://github.com/yt-dlp/yt-dlp)
|
||||
- [awesome-xiaoai](https://github.com/zzz6519003/awesome-xiaoai)
|
||||
- [微信小程序: XIAO晓音](https://github.com/F-loat/xiaoplayer)
|
||||
- [pure 主题 xiaomusicUI](https://github.com/52fisher/xiaomusicUI)
|
||||
- [移动端的播放器主题](https://github.com/52fisher/XMusicPlayer)
|
||||
- [一个第三方的主题](https://github.com/DarrenWen/xiaomusicui)
|
||||
- [Umami 统计](https://github.com/umami-software/umami)
|
||||
- [Sentry 报错监控](https://github.com/getsentry/sentry)
|
||||
- 所有帮忙调试和测试的朋友
|
||||
- 所有反馈问题和建议的朋友
|
||||
|
||||
### 👉 其他教程
|
||||
|
||||
更多功能见 [📝 文档汇总](https://github.com/hanxi/xiaomusic/issues/211)
|
||||
|
||||
## 🚨 免责声明
|
||||
|
||||
本项目仅供学习和研究目的,不得用于任何商业活动。用户在使用本项目时应遵守所在地区的法律法规,对于违法使用所导致的后果,本项目及作者不承担任何责任。
|
||||
本项目可能存在未知的缺陷和风险(包括但不限于设备损坏和账号封禁等),使用者应自行承担使用本项目所产生的所有风险及责任。
|
||||
作者不保证本项目的准确性、完整性、及时性、可靠性,也不承担任何因使用本项目而产生的任何损失或损害责任。
|
||||
使用本项目即表示您已阅读并同意本免责声明的全部内容。
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#hanxi/xiaomusic&Date)
|
||||
|
||||
## 赞赏
|
||||
|
||||
- :moneybag: 爱发电 <https://afdian.com/a/imhanxi>
|
||||
- 点个 Star :star:
|
||||
- 谢谢 :heart:
|
||||
- 
|
||||
|
||||
## License
|
||||
|
||||
[MIT](https://github.com/hanxi/xiaomusic/blob/main/LICENSE) License © 2024 涵曦
|
||||
3680
docs/package-lock.json
generated
Normal file
3680
docs/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
14
docs/package.json
Normal file
14
docs/package.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"vitepress": "^1.5.0"
|
||||
},
|
||||
"scripts": {
|
||||
"docs:dev": "vitepress dev --host=0.0.0.0 --port=3030",
|
||||
"docs:build": "vitepress build",
|
||||
"docs:preview": "vitepress preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.7.9",
|
||||
"vite-plugin-vitepress-auto-sidebar": "^1.7.0"
|
||||
}
|
||||
}
|
||||
64
get_release.py
Normal file
64
get_release.py
Normal file
@@ -0,0 +1,64 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
import requests
|
||||
|
||||
# 替换为你的 GitHub 仓库信息
|
||||
GITHUB_OWNER = "hanxi"
|
||||
GITHUB_REPO = "xiaomusic"
|
||||
GITHUB_API_URL = f"https://api.github.com/repos/{GITHUB_OWNER}/{GITHUB_REPO}/releases"
|
||||
|
||||
|
||||
def fetch_releases():
|
||||
headers = {}
|
||||
github_token = os.getenv("GITHUB_TOKEN")
|
||||
if github_token:
|
||||
headers["Authorization"] = f"token {github_token}"
|
||||
|
||||
try:
|
||||
response = requests.get(GITHUB_API_URL, headers=headers)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.RequestException as e:
|
||||
print(f"请求 GitHub API 失败: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def extract_tar_gz_files(releases):
|
||||
versions = []
|
||||
for release in releases:
|
||||
version = release.get("tag_name")
|
||||
if not version:
|
||||
continue
|
||||
|
||||
files = []
|
||||
for asset in release.get("assets", []):
|
||||
if asset.get("name", "").endswith(".tar.gz"):
|
||||
files.append(asset["name"])
|
||||
|
||||
if files:
|
||||
versions.append({"version": version, "files": files})
|
||||
return versions
|
||||
|
||||
|
||||
def save_to_json(data, filename="versions.json"):
|
||||
try:
|
||||
with open(filename, "w") as f:
|
||||
json.dump(data, f, indent=4)
|
||||
print(f"数据已保存到 {filename}")
|
||||
except OSError as e:
|
||||
print(f"保存文件失败: {e}")
|
||||
|
||||
|
||||
def main():
|
||||
releases = fetch_releases()
|
||||
if not releases:
|
||||
print("未获取到任何 release 数据")
|
||||
return
|
||||
|
||||
versions = extract_tar_gz_files(releases)
|
||||
save_to_json(versions, "docs/.vitepress/dist/versions.json")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
119
pdm.lock
generated
119
pdm.lock
generated
@@ -5,7 +5,7 @@
|
||||
groups = ["default", "dev", "lint"]
|
||||
strategy = ["inherit_metadata"]
|
||||
lock_version = "4.5.0"
|
||||
content_hash = "sha256:37be8846356b5717495cac7252d78b1554985d607e286cf38ab863e307e5e25e"
|
||||
content_hash = "sha256:be03d4a0e0e150847ab753a6b65d34c908944d20c37203ca0e3edba976f0aa80"
|
||||
|
||||
[[metadata.targets]]
|
||||
requires_python = "==3.10.12"
|
||||
@@ -39,7 +39,7 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
version = "3.11.8"
|
||||
version = "3.11.9"
|
||||
requires_python = ">=3.9"
|
||||
summary = "Async http client/server framework (asyncio)"
|
||||
groups = ["default"]
|
||||
@@ -55,8 +55,8 @@ dependencies = [
|
||||
"yarl<2.0,>=1.17.0",
|
||||
]
|
||||
files = [
|
||||
{file = "aiohttp-3.11.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df9bf08eb93611b1d4d6245b6fecf88728e90eece00e00d554e1b0c445557d83"},
|
||||
{file = "aiohttp-3.11.8.tar.gz", hash = "sha256:7bc9d64a2350cbb29a9732334e1a0743cbb6844de1731cbdf5949b235653f3fd"},
|
||||
{file = "aiohttp-3.11.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39625703540feb50b6b7f938b3856d1f4886d2e585d88274e62b1bd273fae09b"},
|
||||
{file = "aiohttp-3.11.9.tar.gz", hash = "sha256:a9266644064779840feec0e34f10a89b3ff1d2d6b751fe90017abcad1864fa7c"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -165,6 +165,17 @@ files = [
|
||||
{file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "1.1.0"
|
||||
summary = "Python bindings for the Brotli compression library"
|
||||
groups = ["default"]
|
||||
marker = "implementation_name == \"cpython\" and python_full_version == \"3.10.12\""
|
||||
files = [
|
||||
{file = "Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da"},
|
||||
{file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2024.8.30"
|
||||
@@ -658,6 +669,18 @@ files = [
|
||||
{file = "propcache-0.2.0.tar.gz", hash = "sha256:df81779732feb9d01e5d513fad0122efb3d53bbc75f61b2a4f29a020bc985e70"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycryptodomex"
|
||||
version = "3.21.0"
|
||||
requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
|
||||
summary = "Cryptographic library for Python"
|
||||
groups = ["default"]
|
||||
marker = "python_full_version == \"3.10.12\""
|
||||
files = [
|
||||
{file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9aa0cf13a1a1128b3e964dc667e5fe5c6235f7d7cfb0277213f0e2a783837cc2"},
|
||||
{file = "pycryptodomex-3.21.0.tar.gz", hash = "sha256:222d0bd05381dd25c32dd6065c071ebf084212ab79bab4599ba9e6a3e0009e6c"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.8.2"
|
||||
@@ -760,14 +783,14 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "python-multipart"
|
||||
version = "0.0.17"
|
||||
version = "0.0.19"
|
||||
requires_python = ">=3.8"
|
||||
summary = "A streaming multipart parser for Python"
|
||||
groups = ["default"]
|
||||
marker = "python_full_version == \"3.10.12\""
|
||||
files = [
|
||||
{file = "python_multipart-0.0.17-py3-none-any.whl", hash = "sha256:15dc4f487e0a9476cc1201261188ee0940165cffc94429b6fc565c4d3045cb5d"},
|
||||
{file = "python_multipart-0.0.17.tar.gz", hash = "sha256:41330d831cae6e2f22902704ead2826ea038d0419530eadff3ea80175aec5538"},
|
||||
{file = "python_multipart-0.0.19-py3-none-any.whl", hash = "sha256:f8d5b0b9c618575bf9df01c684ded1d94a338839bdd8223838afacfb4bb2082d"},
|
||||
{file = "python_multipart-0.0.19.tar.gz", hash = "sha256:905502ef39050557b7a6af411f454bc19526529ca46ae6831508438890ce12cc"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -856,14 +879,47 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
requires_python = ">=3.7"
|
||||
summary = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
groups = ["lint"]
|
||||
marker = "python_full_version == \"3.10.12\""
|
||||
files = [
|
||||
{file = "ruff-0.8.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87a8e86bae0dbd749c815211ca11e3a7bd559b9710746c559ed63106d382bd9c"},
|
||||
{file = "ruff-0.8.0.tar.gz", hash = "sha256:a7ccfe6331bf8c8dad715753e157457faf7351c2b69f62f32c165c2dbcbacd44"},
|
||||
{file = "ruff-0.8.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b22346f845fec132aa39cd29acb94451d030c10874408dbf776af3aaeb53284c"},
|
||||
{file = "ruff-0.8.1.tar.gz", hash = "sha256:3583db9a6450364ed5ca3f3b4225958b24f78178908d5c4bc0f46251ccca898f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-sdk"
|
||||
version = "1.45.1"
|
||||
summary = "Python client for Sentry (https://sentry.io)"
|
||||
groups = ["default"]
|
||||
marker = "python_full_version == \"3.10.12\""
|
||||
dependencies = [
|
||||
"certifi",
|
||||
"urllib3>=1.25.7; python_version <= \"3.4\"",
|
||||
"urllib3>=1.26.11; python_version >= \"3.6\"",
|
||||
"urllib3>=1.26.9; python_version == \"3.5\"",
|
||||
]
|
||||
files = [
|
||||
{file = "sentry_sdk-1.45.1-py2.py3-none-any.whl", hash = "sha256:608887855ccfe39032bfd03936e3a1c4f4fc99b3a4ac49ced54a4220de61c9c1"},
|
||||
{file = "sentry_sdk-1.45.1.tar.gz", hash = "sha256:a16c997c0f4e3df63c0fc5e4207ccb1ab37900433e0f72fef88315d317829a26"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-sdk"
|
||||
version = "1.45.1"
|
||||
extras = ["fastapi"]
|
||||
summary = "Python client for Sentry (https://sentry.io)"
|
||||
groups = ["default"]
|
||||
marker = "python_full_version == \"3.10.12\""
|
||||
dependencies = [
|
||||
"fastapi>=0.79.0",
|
||||
"sentry-sdk==1.45.1",
|
||||
]
|
||||
files = [
|
||||
{file = "sentry_sdk-1.45.1-py2.py3-none-any.whl", hash = "sha256:608887855ccfe39032bfd03936e3a1c4f4fc99b3a4ac49ced54a4220de61c9c1"},
|
||||
{file = "sentry_sdk-1.45.1.tar.gz", hash = "sha256:a16c997c0f4e3df63c0fc5e4207ccb1ab37900433e0f72fef88315d317829a26"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -989,6 +1045,19 @@ files = [
|
||||
{file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "websockets"
|
||||
version = "14.1"
|
||||
requires_python = ">=3.9"
|
||||
summary = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
|
||||
groups = ["default"]
|
||||
marker = "python_full_version == \"3.10.12\""
|
||||
files = [
|
||||
{file = "websockets-14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9607b9a442392e690a57909c362811184ea429585a71061cd5d3c2b98065c199"},
|
||||
{file = "websockets-14.1-py3-none-any.whl", hash = "sha256:4d4fc827a20abe6d544a119896f6b78ee13fe81cbfef416f3f2ddf09a03f0e2e"},
|
||||
{file = "websockets-14.1.tar.gz", hash = "sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yarl"
|
||||
version = "1.17.1"
|
||||
@@ -1009,12 +1078,36 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "yt-dlp"
|
||||
version = "2024.11.18"
|
||||
version = "2024.12.1.232904.dev0"
|
||||
requires_python = ">=3.9"
|
||||
summary = "A feature-rich command-line audio/video downloader"
|
||||
groups = ["default"]
|
||||
marker = "python_full_version == \"3.10.12\""
|
||||
files = [
|
||||
{file = "yt_dlp-2024.11.18-py3-none-any.whl", hash = "sha256:b9741695911dc566498b5f115cdd6b1abbc5be61cb01fd98abe649990a41656c"},
|
||||
{file = "yt_dlp-2024.11.18.tar.gz", hash = "sha256:b8a4c23d3c9afd7e476bcdb87f38b6c0e8e12e3a239d7988f13acb434200f54d"},
|
||||
{file = "yt_dlp-2024.12.1.232904.dev0-py3-none-any.whl", hash = "sha256:67c1fa0986662f2f26ba70789c79c6d04fda45eb530944872f4a47ef4f94047e"},
|
||||
{file = "yt_dlp-2024.12.1.232904.dev0.tar.gz", hash = "sha256:4fcce6637e70bad7c63220e4dd69a3a6b8969cbc17c93bfe569940dd173c0548"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yt-dlp"
|
||||
version = "2024.12.1.232904.dev0"
|
||||
extras = ["default"]
|
||||
requires_python = ">=3.9"
|
||||
summary = "A feature-rich command-line audio/video downloader"
|
||||
groups = ["default"]
|
||||
marker = "python_full_version == \"3.10.12\""
|
||||
dependencies = [
|
||||
"brotli; implementation_name == \"cpython\"",
|
||||
"brotlicffi; implementation_name != \"cpython\"",
|
||||
"certifi",
|
||||
"mutagen",
|
||||
"pycryptodomex",
|
||||
"requests<3,>=2.32.2",
|
||||
"urllib3<3,>=1.26.17",
|
||||
"websockets>=13.0",
|
||||
"yt-dlp==2024.12.1.232904.dev0",
|
||||
]
|
||||
files = [
|
||||
{file = "yt_dlp-2024.12.1.232904.dev0-py3-none-any.whl", hash = "sha256:67c1fa0986662f2f26ba70789c79c6d04fda45eb530944872f4a47ef4f94047e"},
|
||||
{file = "yt_dlp-2024.12.1.232904.dev0.tar.gz", hash = "sha256:4fcce6637e70bad7c63220e4dd69a3a6b8969cbc17c93bfe569940dd173c0548"},
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "xiaomusic"
|
||||
version = "0.3.49"
|
||||
version = "0.3.67"
|
||||
description = "Play Music with xiaomi AI speaker"
|
||||
authors = [
|
||||
{name = "涵曦", email = "im.hanxi@gmail.com"},
|
||||
@@ -9,7 +9,7 @@ dependencies = [
|
||||
"aiohttp>=3.8.6",
|
||||
"miservice-fork>=2.7.0",
|
||||
"mutagen>=1.47.0",
|
||||
"yt-dlp>=2024.11.18",
|
||||
"yt-dlp[default]>=2024.12.1.232904.dev0",
|
||||
"uvicorn>=0.30.1",
|
||||
"fastapi>=0.115.4",
|
||||
"starlette>=0.37.2",
|
||||
@@ -20,6 +20,7 @@ dependencies = [
|
||||
"pillow>=10.4.0",
|
||||
"python-multipart>=0.0.12",
|
||||
"requests>=2.32.3",
|
||||
"sentry-sdk[fastapi]==1.45.1",
|
||||
]
|
||||
requires-python = ">=3.10,<=3.12"
|
||||
readme = "README.md"
|
||||
|
||||
27
supervisor.conf
Normal file
27
supervisor.conf
Normal file
@@ -0,0 +1,27 @@
|
||||
[unix_http_server]
|
||||
file=/var/run/supervisor.sock ; Unix套接字文件路径
|
||||
chmod=0777 ; 套接字权限
|
||||
|
||||
[supervisord]
|
||||
pidfile=/tmp/supervisord.pid ; PID文件路径
|
||||
logfile=/app/supervisord.log ; 日志文件路径
|
||||
logfile_maxbytes=50MB ; 日志文件的最大大小
|
||||
logfile_backups=3 ; 保留的旧日志文件数量
|
||||
loglevel=info ; 日志级别
|
||||
|
||||
|
||||
[rpcinterface:supervisor]
|
||||
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
|
||||
|
||||
[supervisorctl]
|
||||
serverurl=unix:///var/run/supervisor.sock ; 使用Unix套接字与服务通信
|
||||
|
||||
[program:xiaomusic]
|
||||
command=/app/.venv/bin/python3 /app/xiaomusic.py
|
||||
directory=/app
|
||||
autostart=true
|
||||
autorestart=true
|
||||
startretries=99999
|
||||
startsecs=60
|
||||
stderr_logfile=AUTO ; 将错误日志输出到 supervisord 控制台
|
||||
stdout_logfile=AUTO ; 将标准日志输出到 supervisord 控制台
|
||||
@@ -1,8 +1,29 @@
|
||||
from xiaomusic.utils import (
|
||||
remove_common_prefix,
|
||||
)
|
||||
import re
|
||||
|
||||
|
||||
def removepre(filename):
|
||||
match = re.search(r"^(\d+)\s+\d*(.+?)\.(.*$)", filename.strip())
|
||||
new_filename = filename
|
||||
if match:
|
||||
num = match.group(1)
|
||||
name = match.group(2).replace(".", " ").strip()
|
||||
suffix = match.group(3)
|
||||
# print(name)
|
||||
# print(num)
|
||||
# print(suffix)
|
||||
new_filename = f"{num}.{name}.{suffix}"
|
||||
|
||||
print(filename, "=>", new_filename)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
remove_common_prefix(
|
||||
"./tmp/【无损音质】2024年9月酷狗热歌榜TOP100合集(只选热歌最高的)首首王炸,分P合集!"
|
||||
)
|
||||
removepre(" 17 《白色风车》.mp3")
|
||||
removepre(" 17 《白色风车》.mp3")
|
||||
removepre(" 17 17 《白色风车》.mp3")
|
||||
removepre(" 17 17 《白色风车》.mp3")
|
||||
|
||||
removepre(" 18 风车.mp3")
|
||||
removepre(" 18 色风车.mp3")
|
||||
removepre(" 18 18 你好.mp3")
|
||||
removepre(" 18 18 我好.mp3")
|
||||
removepre("09 009. 梁静茹-亲亲.mp3")
|
||||
|
||||
10
test/test_update.py
Normal file
10
test/test_update.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from xiaomusic.utils import (
|
||||
download_and_extract,
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
|
||||
url = "https://github.hanxi.cc/proxy/hanxi/xiaomusic/releases/download/main/app-amd64-lite.tar.gz"
|
||||
target_directory = "./tmp/app"
|
||||
asyncio.run(download_and_extract(url, target_directory))
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
from xiaomusic.cli import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
from xiaomusic.cli import main
|
||||
|
||||
main()
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.3.49"
|
||||
__version__ = "0.3.67"
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import asyncio
|
||||
import copy
|
||||
import platform
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
|
||||
import aiohttp
|
||||
from ga4mp import GtagMP
|
||||
|
||||
from xiaomusic import __version__
|
||||
|
||||
|
||||
class Analytics:
|
||||
def __init__(self, log):
|
||||
def __init__(self, log, config):
|
||||
self.gtag = None
|
||||
self.current_date = None
|
||||
self.log = log
|
||||
self.config = config
|
||||
self.init()
|
||||
|
||||
def init(self):
|
||||
@@ -27,34 +32,12 @@ class Analytics:
|
||||
self.gtag = gtag
|
||||
self.log.info("analytics init ok")
|
||||
|
||||
async def run_with_cancel(self, func, *args, **kwargs):
|
||||
try:
|
||||
asyncio.ensure_future(asyncio.to_thread(func, *args, **kwargs))
|
||||
except Exception as e:
|
||||
self.log.warning(f"analytics run_with_cancel failed {e}")
|
||||
return None
|
||||
|
||||
async def send_startup_event(self):
|
||||
try:
|
||||
await self.run_with_cancel(self._send_startup_event)
|
||||
except Exception as e:
|
||||
self.log.warning(f"analytics send_startup_event failed {e}")
|
||||
self.init()
|
||||
|
||||
def _send_startup_event(self):
|
||||
event = self.gtag.create_new_event(name="startup")
|
||||
event.set_event_param(name="version", value=__version__)
|
||||
event_list = [event]
|
||||
self.gtag.send(events=event_list)
|
||||
await self._send(event)
|
||||
|
||||
async def send_daily_event(self):
|
||||
try:
|
||||
await self.run_with_cancel(self._send_daily_event)
|
||||
except Exception as e:
|
||||
self.log.warning(f"analytics send_daily_event failed {e}")
|
||||
self.init()
|
||||
|
||||
def _send_daily_event(self):
|
||||
current_date = datetime.now().strftime("%Y-%m-%d")
|
||||
if self.current_date == current_date:
|
||||
return
|
||||
@@ -62,21 +45,93 @@ class Analytics:
|
||||
event = self.gtag.create_new_event(name="daily_active_user")
|
||||
event.set_event_param(name="version", value=__version__)
|
||||
event.set_event_param(name="date", value=current_date)
|
||||
event_list = [event]
|
||||
self.gtag.send(events=event_list)
|
||||
await self._send(event)
|
||||
self.current_date = current_date
|
||||
|
||||
async def send_play_event(self, name, sec):
|
||||
try:
|
||||
await self.run_with_cancel(self._send_play_event, name, sec)
|
||||
except Exception as e:
|
||||
self.log.warning(f"analytics send_play_event failed {e}")
|
||||
self.init()
|
||||
|
||||
def _send_play_event(self, name, sec):
|
||||
async def send_play_event(self, name, sec, hardware):
|
||||
event = self.gtag.create_new_event(name="play")
|
||||
event.set_event_param(name="version", value=__version__)
|
||||
event.set_event_param(name="music", value=name)
|
||||
event.set_event_param(name="sec", value=sec)
|
||||
event_list = [event]
|
||||
self.gtag.send(events=event_list)
|
||||
event.set_event_param(name="hardware", value=hardware)
|
||||
await self._send(event)
|
||||
|
||||
async def _send(self, event):
|
||||
await self.post_to_umami(event)
|
||||
events = [event]
|
||||
await self.run_with_cancel(self._google_send, events)
|
||||
|
||||
async def _google_send(self, events):
|
||||
try:
|
||||
self.gtag.send(events)
|
||||
except Exception as e:
|
||||
self.log.warning(f"google analytics run_with_cancel failed {e}")
|
||||
|
||||
async def run_with_cancel(self, func, *args, **kwargs):
|
||||
try:
|
||||
asyncio.ensure_future(asyncio.to_thread(func, *args, **kwargs))
|
||||
self.log.info("analytics run_with_cancel success")
|
||||
except Exception as e:
|
||||
self.log.warning(f"analytics run_with_cancel failed {e}")
|
||||
return None
|
||||
|
||||
async def post_to_umami(self, event):
|
||||
try:
|
||||
url = "https://umami.hanxi.cc/api/send"
|
||||
user_agent = self._get_user_agent()
|
||||
params = copy.copy(event.get_event_params())
|
||||
params["useragent"] = user_agent
|
||||
data = {
|
||||
"payload": {
|
||||
"hostname": self.config.hostname,
|
||||
"language": "zh-CN",
|
||||
"referrer": "",
|
||||
"screen": "430x932",
|
||||
"title": "后端统计",
|
||||
"url": "/backend",
|
||||
"website": "7bfb0890-4115-4260-8892-b391513e7e99",
|
||||
"name": event.get_event_name(),
|
||||
"data": params,
|
||||
},
|
||||
"type": "event",
|
||||
}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
headers = {
|
||||
"User-Agent": user_agent,
|
||||
}
|
||||
# self.log.info(f"headers {headers}, {data}")
|
||||
async with session.post(url, json=data, headers=headers) as response:
|
||||
self.log.info(f"umami Status: {response.status}")
|
||||
await response.text()
|
||||
except Exception as e:
|
||||
self.log.exception(f"Execption {e}")
|
||||
|
||||
def _get_user_agent(self):
|
||||
try:
|
||||
# 获取系统信息
|
||||
os_name = platform.system() # 操作系统名称,如 'Windows', 'Linux', 'Darwin'
|
||||
os_version = platform.version() # 操作系统版本号
|
||||
architecture = "unknow"
|
||||
try:
|
||||
architecture = platform.architecture()[0] # '32bit' or '64bit'
|
||||
except Exception as e:
|
||||
architecture = f"Error {e}"
|
||||
pass
|
||||
machine = platform.machine() # 机器类型,如 'x86_64', 'arm64'
|
||||
|
||||
# 获取 Python 版本信息
|
||||
python_version = platform.python_version() # Python 版本
|
||||
|
||||
# 组合 User-Agent 字符串
|
||||
user_agent = (
|
||||
f"XiaoMusic/{__version__} "
|
||||
f"({os_name} {os_version}; {architecture}; {machine}) "
|
||||
f"Python/{python_version}"
|
||||
)
|
||||
except Exception as e:
|
||||
# 获取报错的堆栈信息
|
||||
error_info = traceback.format_exc()
|
||||
user_agent = f"Error: {e} {error_info}"
|
||||
|
||||
return user_agent
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
|
||||
import uvicorn
|
||||
|
||||
from xiaomusic import __version__
|
||||
from xiaomusic.config import Config
|
||||
from xiaomusic.httpserver import HttpInit
|
||||
from xiaomusic.httpserver import app as HttpApp
|
||||
from xiaomusic.xiaomusic import XiaoMusic
|
||||
import sentry_sdk
|
||||
from sentry_sdk.integrations.asyncio import AsyncioIntegration
|
||||
from sentry_sdk.integrations.logging import LoggingIntegration, ignore_logger
|
||||
|
||||
LOGO = r"""
|
||||
__ __ _ __ __ _
|
||||
@@ -22,7 +19,30 @@ LOGO = r"""
|
||||
"""
|
||||
|
||||
|
||||
sentry_sdk.init(
|
||||
# dsn="https://659690a901a37237df8097a9eb95e60f@github.hanxi.cc/sentry/4508470200434688",
|
||||
dsn="https://ffe4962642d04b29afe62ebd1a065231@glitchtip.hanxi.cc/1",
|
||||
integrations=[
|
||||
AsyncioIntegration(),
|
||||
LoggingIntegration(
|
||||
level=logging.WARNING,
|
||||
event_level=logging.ERROR,
|
||||
),
|
||||
],
|
||||
# debug=True,
|
||||
)
|
||||
ignore_logger("miservice")
|
||||
|
||||
|
||||
def main():
|
||||
import uvicorn
|
||||
|
||||
from xiaomusic import __version__
|
||||
from xiaomusic.config import Config
|
||||
from xiaomusic.httpserver import HttpInit
|
||||
from xiaomusic.httpserver import app as HttpApp
|
||||
from xiaomusic.xiaomusic import XiaoMusic
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
|
||||
@@ -75,7 +75,7 @@ class Device:
|
||||
device_id: str = ""
|
||||
hardware: str = ""
|
||||
name: str = ""
|
||||
play_type: int = ""
|
||||
play_type: int = PLAY_TYPE_RND
|
||||
cur_music: str = ""
|
||||
cur_playlist: str = ""
|
||||
|
||||
@@ -103,9 +103,10 @@ class Config:
|
||||
ffmpeg_location: str = os.getenv("XIAOMUSIC_FFMPEG_LOCATION", "./ffmpeg/bin")
|
||||
active_cmd: str = os.getenv(
|
||||
"XIAOMUSIC_ACTIVE_CMD",
|
||||
"play,set_play_type_rnd,playlocal,play_music_list,play_music_list_index,stop_after_minute,stop",
|
||||
"play,search_play,set_play_type_rnd,playlocal,search_playlocal,play_music_list,play_music_list_index,stop_after_minute,stop",
|
||||
)
|
||||
exclude_dirs: str = os.getenv("XIAOMUSIC_EXCLUDE_DIRS", "@eaDir,tmp")
|
||||
ignore_tag_dirs: str = os.getenv("XIAOMUSIC_IGNORE_TAG_DIRS", "")
|
||||
music_path_depth: int = int(os.getenv("XIAOMUSIC_MUSIC_PATH_DEPTH", "10"))
|
||||
disable_httpauth: bool = (
|
||||
os.getenv("XIAOMUSIC_DISABLE_HTTPAUTH", "true").lower() == "true"
|
||||
@@ -127,7 +128,7 @@ class Config:
|
||||
"XIAOMUSIC_USE_MUSIC_AUDIO_ID", "1582971365183456177"
|
||||
)
|
||||
use_music_id: str = os.getenv("XIAOMUSIC_USE_MUSIC_ID", "355454500")
|
||||
log_file: str = os.getenv("XIAOMUSIC_LOG_FILE", "/tmp/xiaomusic.txt")
|
||||
log_file: str = os.getenv("XIAOMUSIC_LOG_FILE", "xiaomusic.log.txt")
|
||||
# 模糊搜索匹配的最低相似度阈值
|
||||
fuzzy_match_cutoff: float = float(os.getenv("XIAOMUSIC_FUZZY_MATCH_CUTOFF", "0.6"))
|
||||
# 开启模糊搜索
|
||||
@@ -140,7 +141,11 @@ class Config:
|
||||
keywords_playlocal: str = os.getenv(
|
||||
"XIAOMUSIC_KEYWORDS_PLAYLOCAL", "播放本地歌曲,本地播放歌曲"
|
||||
)
|
||||
keywords_search_playlocal: str = os.getenv(
|
||||
"XIAOMUSIC_KEYWORDS_SEARCH_PLAYLOCAL", "本地搜索播放"
|
||||
)
|
||||
keywords_play: str = os.getenv("XIAOMUSIC_KEYWORDS_PLAY", "播放歌曲,放歌曲")
|
||||
keywords_search_play: str = os.getenv("XIAOMUSIC_KEYWORDS_SEARCH_PLAY", "搜索播放")
|
||||
keywords_stop: str = os.getenv("XIAOMUSIC_KEYWORDS_STOP", "关机,暂停,停止,停止播放")
|
||||
keywords_playlist: str = os.getenv(
|
||||
"XIAOMUSIC_KEYWORDS_PLAYLIST", "播放列表,播放歌单"
|
||||
@@ -168,6 +173,9 @@ class Config:
|
||||
enable_yt_dlp_cookies: bool = (
|
||||
os.getenv("XIAOMUSIC_ENABLE_YT_DLP_COOKIES", "false").lower() == "true"
|
||||
)
|
||||
enable_save_tag: bool = (
|
||||
os.getenv("XIAOMUSIC_ENABLE_SAVE_TAG", "false").lower() == "true"
|
||||
)
|
||||
get_ask_by_mina: bool = (
|
||||
os.getenv("XIAOMUSIC_GET_ASK_BY_MINA", "false").lower() == "true"
|
||||
)
|
||||
@@ -186,6 +194,9 @@ class Config:
|
||||
play_type_seq_tts_msg: str = os.getenv(
|
||||
"XIAOMUSIC_PLAY_TYPE_SEQ_TTS_MSG", "已经设置为顺序播放"
|
||||
)
|
||||
recently_added_playlist_len: int = int(
|
||||
os.getenv("XIAOMUSIC_RECENTLY_ADDED_PLAYLIST_LEN", "50")
|
||||
)
|
||||
|
||||
def append_keyword(self, keys, action):
|
||||
for key in keys.split(","):
|
||||
@@ -204,7 +215,9 @@ class Config:
|
||||
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_search_playlocal, "search_playlocal")
|
||||
self.append_keyword(self.keywords_play, "play")
|
||||
self.append_keyword(self.keywords_search_play, "search_play")
|
||||
self.append_keyword(self.keywords_stop, "stop")
|
||||
self.append_keyword(self.keywords_playlist, "play_music_list")
|
||||
self.append_user_keyword()
|
||||
@@ -324,3 +337,11 @@ class Config:
|
||||
if play_type == PLAY_TYPE_SEQ:
|
||||
return self.play_type_seq_tts_msg
|
||||
return ""
|
||||
|
||||
def get_ignore_tag_dirs(self):
|
||||
ignore_tag_absolute_dirs = []
|
||||
for ignore_tag_dir in self.ignore_tag_dirs.split(","):
|
||||
if ignore_tag_dir:
|
||||
ignore_tag_absolute_path = os.path.abspath(ignore_tag_dir)
|
||||
ignore_tag_absolute_dirs.append(ignore_tag_absolute_path)
|
||||
return ignore_tag_absolute_dirs
|
||||
|
||||
@@ -20,3 +20,10 @@ PLAY_TYPE_SEQ = 4 # 顺序播放
|
||||
GET_ASK_BY_MINA = {
|
||||
"M01",
|
||||
}
|
||||
|
||||
# 需要使用 play_musci 接口的设备型号
|
||||
NEED_USE_PLAY_MUSIC_API = {
|
||||
"X08C",
|
||||
"X08E",
|
||||
"X8F",
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
import shutil
|
||||
import tempfile
|
||||
@@ -30,10 +29,12 @@ from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from pydantic import BaseModel
|
||||
from starlette.background import BackgroundTask
|
||||
from starlette.middleware.gzip import GZipMiddleware
|
||||
from starlette.responses import FileResponse, Response
|
||||
|
||||
from xiaomusic import __version__
|
||||
from xiaomusic.utils import (
|
||||
chmoddir,
|
||||
convert_file_to_mp3,
|
||||
deepcopy_data_no_sensitive_info,
|
||||
download_one_music,
|
||||
@@ -43,7 +44,9 @@ from xiaomusic.utils import (
|
||||
is_mp3,
|
||||
remove_common_prefix,
|
||||
remove_id3_tags,
|
||||
restart_xiaomusic,
|
||||
try_add_access_control_param,
|
||||
update_version,
|
||||
)
|
||||
|
||||
xiaomusic = None
|
||||
@@ -105,6 +108,8 @@ app.add_middleware(
|
||||
allow_methods=["*"], # 允许使用的请求方法
|
||||
allow_headers=["*"], # 允许携带的 Headers
|
||||
)
|
||||
# 添加 GZip 中间件
|
||||
app.add_middleware(GZipMiddleware, minimum_size=500)
|
||||
|
||||
|
||||
def reset_http_server():
|
||||
@@ -303,6 +308,23 @@ async def musicinfos(
|
||||
return ret
|
||||
|
||||
|
||||
class MusicInfoObj(BaseModel):
|
||||
musicname: str
|
||||
title: str = ""
|
||||
artist: str = ""
|
||||
album: str = ""
|
||||
year: str = ""
|
||||
genre: str = ""
|
||||
lyrics: str = ""
|
||||
picture: str = "" # base64
|
||||
|
||||
|
||||
@app.post("/setmusictag")
|
||||
async def setmusictag(info: MusicInfoObj, Verifcation=Depends(verification)):
|
||||
ret = xiaomusic.set_music_tag(info.musicname, info)
|
||||
return {"ret": ret}
|
||||
|
||||
|
||||
@app.get("/curplaylist")
|
||||
async def curplaylist(did: str = "", Verifcation=Depends(verification)):
|
||||
if not xiaomusic.did_exist(did):
|
||||
@@ -325,6 +347,44 @@ class UrlInfo(BaseModel):
|
||||
url: str
|
||||
|
||||
|
||||
class DidPlayMusic(BaseModel):
|
||||
did: str
|
||||
musicname: str = ""
|
||||
searchkey: str = ""
|
||||
|
||||
|
||||
@app.post("/playmusic")
|
||||
async def playmusic(data: DidPlayMusic, Verifcation=Depends(verification)):
|
||||
did = data.did
|
||||
musicname = data.musicname
|
||||
searchkey = data.searchkey
|
||||
if not xiaomusic.did_exist(did):
|
||||
return {"ret": "Did not exist"}
|
||||
|
||||
log.info(f"playmusic {did} musicname:{musicname} searchkey:{searchkey}")
|
||||
await xiaomusic.do_play(did, musicname, searchkey)
|
||||
return {"ret": "OK"}
|
||||
|
||||
|
||||
class DidPlayMusicList(BaseModel):
|
||||
did: str
|
||||
listname: str = ""
|
||||
musicname: str = ""
|
||||
|
||||
|
||||
@app.post("/playmusiclist")
|
||||
async def playmusiclist(data: DidPlayMusicList, Verifcation=Depends(verification)):
|
||||
did = data.did
|
||||
listname = data.listname
|
||||
musicname = data.musicname
|
||||
if not xiaomusic.did_exist(did):
|
||||
return {"ret": "Did not exist"}
|
||||
|
||||
log.info(f"playmusiclist {did} listname:{listname} musicname:{musicname}")
|
||||
await xiaomusic.do_play_music_list(did, listname, musicname)
|
||||
return {"ret": "OK"}
|
||||
|
||||
|
||||
@app.post("/downloadjson")
|
||||
async def downloadjson(data: UrlInfo, Verifcation=Depends(verification)):
|
||||
log.info(data)
|
||||
@@ -430,6 +490,7 @@ async def downloadplaylist(data: DownloadPlayList, Verifcation=Depends(verificat
|
||||
log.debug(f"Download dir_path: {dir_path}")
|
||||
# 可能只是部分失败,都需要整理下载目录
|
||||
remove_common_prefix(dir_path)
|
||||
chmoddir(dir_path)
|
||||
|
||||
asyncio.create_task(check_download_proc())
|
||||
return {"ret": "OK"}
|
||||
@@ -448,7 +509,15 @@ class DownloadOneMusic(BaseModel):
|
||||
@app.post("/downloadonemusic")
|
||||
async def downloadonemusic(data: DownloadOneMusic, Verifcation=Depends(verification)):
|
||||
try:
|
||||
await download_one_music(config, data.url, data.name)
|
||||
download_proc = await download_one_music(config, data.url, data.name)
|
||||
|
||||
async def check_download_proc():
|
||||
# 等待子进程完成
|
||||
exit_code = await download_proc.wait()
|
||||
log.info(f"Download completed with exit code {exit_code}")
|
||||
chmoddir(config.download_path)
|
||||
|
||||
asyncio.create_task(check_download_proc())
|
||||
return {"ret": "OK"}
|
||||
except Exception as e:
|
||||
log.exception(f"Execption {e}")
|
||||
@@ -490,6 +559,33 @@ async def playlistdel(data: PlayListObj, Verifcation=Depends(verification)):
|
||||
return {"ret": "Del failed, may be not exist."}
|
||||
|
||||
|
||||
class PlayListUpdateObj(BaseModel):
|
||||
oldname: str # 旧歌单名字
|
||||
newname: str # 新歌单名字
|
||||
|
||||
|
||||
# 修改歌单名字
|
||||
@app.post("/playlistupdatename")
|
||||
async def playlistupdatename(
|
||||
data: PlayListUpdateObj, Verifcation=Depends(verification)
|
||||
):
|
||||
ret = xiaomusic.play_list_update_name(data.oldname, data.newname)
|
||||
if ret:
|
||||
return {"ret": "OK"}
|
||||
return {"ret": "Update failed, may be not exist."}
|
||||
|
||||
|
||||
# 获取所有自定义歌单
|
||||
@app.get("/playlistnames")
|
||||
async def getplaylistnames(Verifcation=Depends(verification)):
|
||||
names = xiaomusic.get_play_list_names()
|
||||
log.info(f"names {names}")
|
||||
return {
|
||||
"ret": "OK",
|
||||
"names": names,
|
||||
}
|
||||
|
||||
|
||||
class PlayListMusicObj(BaseModel):
|
||||
name: str = "" # 歌单名
|
||||
music_list: list[str] # 歌曲名列表
|
||||
@@ -513,6 +609,40 @@ async def playlistdelmusic(data: PlayListMusicObj, Verifcation=Depends(verificat
|
||||
return {"ret": "Del failed, may be playlist not exist."}
|
||||
|
||||
|
||||
# 歌单更新歌曲
|
||||
@app.post("/playlistupdatemusic")
|
||||
async def playlistupdatemusic(
|
||||
data: PlayListMusicObj, Verifcation=Depends(verification)
|
||||
):
|
||||
ret = xiaomusic.play_list_update_music(data.name, data.music_list)
|
||||
if ret:
|
||||
return {"ret": "OK"}
|
||||
return {"ret": "Del failed, may be playlist not exist."}
|
||||
|
||||
|
||||
# 获取歌单中所有歌曲
|
||||
@app.get("/playlistmusics")
|
||||
async def getplaylist(name: str, Verifcation=Depends(verification)):
|
||||
ret, musics = xiaomusic.play_list_musics(name)
|
||||
return {
|
||||
"ret": "OK",
|
||||
"musics": musics,
|
||||
}
|
||||
|
||||
|
||||
# 更新版本
|
||||
@app.post("/updateversion")
|
||||
async def updateversion(
|
||||
version: str = "", lite: bool = True, Verifcation=Depends(verification)
|
||||
):
|
||||
ret = await update_version(version, lite)
|
||||
if ret != "OK":
|
||||
return {"ret": ret}
|
||||
|
||||
asyncio.create_task(restart_xiaomusic())
|
||||
return {"ret": "OK"}
|
||||
|
||||
|
||||
async def file_iterator(file_path, start, end):
|
||||
async with aiofiles.open(file_path, mode="rb") as file:
|
||||
await file.seek(start)
|
||||
@@ -558,9 +688,6 @@ def access_key_verification(file_path, key, code):
|
||||
return False
|
||||
|
||||
|
||||
range_pattern = re.compile(r"bytes=(\d+)-(\d*)")
|
||||
|
||||
|
||||
def safe_redirect(url):
|
||||
url = try_add_access_control_param(config, url)
|
||||
url = url.replace("\\", "")
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>Debug For XiaoMusic</title>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="./style.css?version=1732797036">
|
||||
<link rel="stylesheet" type="text/css" href="./main.css?version=1735438766">
|
||||
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
|
||||
<script src="./jquery-3.7.1.min.js?version=1732797036"></script>
|
||||
<script src="./jquery-3.7.1.min.js?version=1735438766"></script>
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>
|
||||
@@ -19,6 +19,9 @@
|
||||
gtag('config', 'G-Z09NC1K7ZW');
|
||||
</script>
|
||||
|
||||
<!-- umami -->
|
||||
<script defer src="https://umami.hanxi.cc/script.js" data-website-id="7bfb0890-4115-4260-8892-b391513e7e99"></script>
|
||||
|
||||
<script>
|
||||
var vConsole = new window.VConsole();
|
||||
|
||||
@@ -57,15 +60,19 @@ function sendDebugCmd() {
|
||||
</head>
|
||||
<body>
|
||||
<h1>Debug For XiaoMusic</h1>
|
||||
<textarea id="post-input" rows="10" cols="50" placeholder="粘贴json数据..."></textarea><br>
|
||||
<button onclick="postJSON()">提交</button><br>
|
||||
|
||||
<div class="debug">
|
||||
<textarea id="post-input" rows="10" cols="50" placeholder="粘贴json数据..."></textarea><br>
|
||||
<button onclick="postJSON()">提交</button><br>
|
||||
</div>
|
||||
<hr>
|
||||
<input id="cmd" type="text"></input>
|
||||
<button onclick="sendDebugCmd()">测试自定义口令</button><br>
|
||||
<div class="debug">
|
||||
<input id="cmd" type="text"></input>
|
||||
<button onclick="sendDebugCmd()">测试自定义口令</button><br>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
<footer>
|
||||
<p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p>
|
||||
<p>Powered by <a href="https://xdocs.hanxi.cc" target="_blank">XiaoMusic</a></p>
|
||||
</footer>
|
||||
</html>
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>歌曲下载工具</title>
|
||||
<link rel="stylesheet" type="text/css" href="./style.css?version=1732797036">
|
||||
<script src="./jquery-3.7.1.min.js?version=1732797036"></script>
|
||||
<link rel="stylesheet" type="text/css" href="./main.css?version=1735438766">
|
||||
<script src="./jquery-3.7.1.min.js?version=1735438766"></script>
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>
|
||||
@@ -16,6 +16,9 @@
|
||||
gtag('config', 'G-Z09NC1K7ZW');
|
||||
</script>
|
||||
|
||||
<!-- umami -->
|
||||
<script defer src="https://umami.hanxi.cc/script.js" data-website-id="7bfb0890-4115-4260-8892-b391513e7e99"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
||||
BIN
xiaomusic/static/default/favicon.ico
Normal file
BIN
xiaomusic/static/default/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 894 B |
@@ -1,12 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<html lang="zh">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>小爱音箱操控面板</title>
|
||||
<script src="./jquery-3.7.1.min.js?version=1732797036"></script>
|
||||
<script src="./app.js?version=1732797036"></script>
|
||||
<link rel="stylesheet" type="text/css" href="./style.css?version=1732797036">
|
||||
<link href="https://fonts.googleapis.com/css?family=Material+Icons|Material+Icons+Outlined" rel="stylesheet">
|
||||
<script src="./jquery-3.7.1.min.js?version=1735438766"></script>
|
||||
<link rel="stylesheet" href="./main.css?version=1735438766">
|
||||
<link rel="icon" href="./favicon.ico">
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>
|
||||
@@ -17,82 +19,209 @@
|
||||
gtag('config', 'G-Z09NC1K7ZW');
|
||||
</script>
|
||||
|
||||
<!--
|
||||
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
|
||||
<script>
|
||||
var vConsole = new window.VConsole();
|
||||
</script>
|
||||
<!-- umami -->
|
||||
<script defer src="https://umami.hanxi.cc/script.js" data-website-id="7bfb0890-4115-4260-8892-b391513e7e99"></script>
|
||||
</head>
|
||||
|
||||
<body class="index_page">
|
||||
<div class="player">
|
||||
<h1>小爱音箱播放器
|
||||
<a id="version" href="javascript:void(0);">1.0.0</a><span id="versionnew" class="new-badge"></span>
|
||||
</h1>
|
||||
|
||||
<label for="did">选择播放设备:</label>
|
||||
<select id="did" class="device-selector">
|
||||
<option value="default">默认设备</option>
|
||||
</select>
|
||||
|
||||
<label for="music_list" style="display: flex;align-items: center;">选择播放列表:
|
||||
<div class="option-inline" onclick="sendcmd('刷新列表')">
|
||||
<span class="material-icons">refresh</span>
|
||||
<span class="tooltip">刷新列表</span>
|
||||
</div>
|
||||
</label>
|
||||
<select id="music_list" class="playlist-selector">
|
||||
</select>
|
||||
|
||||
<label for="music_name" style="display: flex;align-items: center;">选择歌曲:
|
||||
<div class="option-inline" onclick="toggleDelete()">
|
||||
<span class="material-icons">delete</span>
|
||||
<span class="tooltip">删除歌曲</span>
|
||||
</div>
|
||||
</label>
|
||||
<select id="music_name" class="song-selector">
|
||||
</select>
|
||||
|
||||
<div id="device-audio">
|
||||
<progress class="progress" id="progress" value="0" max="100"></progress>
|
||||
<div style="display: flex; justify-content: space-between; width: 100%;">
|
||||
<span class="current-time" id="current-time">0:00</span>
|
||||
<div class="current-song" id="playering-music">当前播放歌曲:无</div>
|
||||
<span class="duration" id="duration">00:00</span>
|
||||
</div>
|
||||
</div>
|
||||
<audio id="audio" controls src="" autoplay></audio>
|
||||
|
||||
<div class="buttons">
|
||||
<div class="player-controls button-group">
|
||||
<div id="modeBtn" onclick="togglePlayMode()" class="control-button device-enable">
|
||||
<span class="material-icons">shuffle</span>
|
||||
<span class="tooltip">切换播放模式</span>
|
||||
</div>
|
||||
<div onclick="prevTrack()" class="control-button device-enable">
|
||||
<span class="material-icons">skip_previous</span>
|
||||
<span class="tooltip">上一首</span>
|
||||
</div>
|
||||
<div onclick="play()" class="control-button">
|
||||
<span class="material-icons-outlined play">play_circle_outline</span>
|
||||
<span class="tooltip">播放</span>
|
||||
</div>
|
||||
<div onclick="nextTrack()" class="control-button device-enable">
|
||||
<span class="material-icons">skip_next</span>
|
||||
<span class="tooltip">下一首</span>
|
||||
</div>
|
||||
<div onclick="stopPlay()" class="control-button device-enable">
|
||||
<span class="material-icons">stop</span>
|
||||
<span class="tooltip">关机</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="mode-controls button-group">
|
||||
<div onclick="addToFavorites()" class="favorite icon-item device-enable">
|
||||
<span class="material-icons">favorite</span>
|
||||
<p>收藏</p>
|
||||
</div>
|
||||
<div onclick="toggleVolume()" class="icon-item device-enable">
|
||||
<span class="material-icons">volume_up</span>
|
||||
<p>音量</p>
|
||||
</div>
|
||||
<!--
|
||||
<div onclick="toggleLocalPlay()" id="web_play">
|
||||
<span class="material-icons">headphones</span>
|
||||
<span class="tooltip">网页播放</span>
|
||||
</div>
|
||||
-->
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h2>小爱音箱操控面板
|
||||
(<a id="version" href="https://github.com/hanxi/xiaomusic/blob/main/CHANGELOG.md">版本未知</a>)
|
||||
<span id="versionnew" class="blink"></span>
|
||||
</h2>
|
||||
<hr>
|
||||
|
||||
<div class="rows">
|
||||
<select id="did">
|
||||
</select>
|
||||
<div onclick="toggleSearch()" class="icon-item device-enable">
|
||||
<span class="material-icons">search</span>
|
||||
<p>搜索</p>
|
||||
</div>
|
||||
<div onclick="togglePlayLink()" class="icon-item device-enable">
|
||||
<span class="material-icons">link</span>
|
||||
<p>链接</p>
|
||||
</div>
|
||||
<div onclick="toggleTimer()" class="icon-item device-enable">
|
||||
<span class="material-icons">timer</span>
|
||||
<p>定时</p>
|
||||
</div>
|
||||
<div onclick="openSettings()" class="icon-item">
|
||||
<span class="material-icons">settings</span>
|
||||
<p>设置</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="cmds">
|
||||
<a class="button" href="./setting.html">设置</a>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<div style="margin: 20px;">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="#8e43e7" style="height: 48px; width: 48px;"><path d="M550.826667 154.666667a47.786667 47.786667 0 0 0-19.84 4.48L298.666667 298.666667H186.453333A80 80 0 0 0 106.666667 378.453333v267.093334A80 80 0 0 0 186.453333 725.333333H298.666667l232.32 139.52a47.786667 47.786667 0 0 0 19.84 4.48A46.506667 46.506667 0 0 0 597.333333 822.826667V201.173333a46.506667 46.506667 0 0 0-46.506666-46.506666zM554.666667 822.826667c0 3.413333-3.84 3.84-3.84 3.84L320 688.853333l-9.6-6.186666H186.453333A37.12 37.12 0 0 1 149.333333 645.546667V378.453333A37.12 37.12 0 0 1 186.453333 341.333333h123.946667l10.24-6.186666 229.546667-137.6s3.84 0 3.84 3.84zM667.52 346.026667a21.333333 21.333333 0 0 0 0 30.293333 192 192 0 0 1 0 271.36 21.333333 21.333333 0 0 0 0 30.293333 21.333333 21.333333 0 0 0 30.293333 0 234.666667 234.666667 0 0 0 0-331.946666 21.333333 21.333333 0 0 0-30.293333 0z"></path><path d="M804.48 219.52a21.333333 21.333333 0 0 0-30.293333 30.293333 370.986667 370.986667 0 0 1 0 524.373334 21.333333 21.333333 0 0 0 0 30.293333 21.333333 21.333333 0 0 0 30.293333 0 414.08 414.08 0 0 0 0-584.96z"></path></svg>
|
||||
<input id="volume" type="range"></input>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="rows">
|
||||
<label for="search">搜索歌曲:</label>
|
||||
<input type="text" id="search" placeholder="请输入搜索关键词(如:MV高清版 周杰伦 七里香)">
|
||||
|
||||
<label for="music-name" id="music-name-label" style="display: none;">确认选择:</label>
|
||||
<select id="music-name" style="display: none;">
|
||||
<!-- 动态生成选项 -->
|
||||
</select>
|
||||
|
||||
<input id="music-filename" type="text" placeholder="请输入保存为的文件名称(如:周杰伦七里香)" style="display: none;"></input>
|
||||
<div style="display: flex; align-items: center">
|
||||
<progress id="progress" value="0" max="100" style="width: 270px"></progress>
|
||||
<div id="play-time" style="margin-left: 10px">00:00/00:00</div>
|
||||
</div>
|
||||
<div>
|
||||
<button id="play">播放</button>
|
||||
<div id="playering-music" class="text"></div>
|
||||
</div>
|
||||
<!-- 搜索组件 -->
|
||||
<div class="component" id="search-component">
|
||||
<h2>搜索歌曲</h2>
|
||||
<input type="text" id="search" class="search-input" placeholder="请输入搜索关键词(如:MV高清版 周杰伦 七里香)">
|
||||
<label for="music-name" id="music-name-label" style="display: none;">确认选择:</label>
|
||||
<select id="music-name" style="display: none;">
|
||||
<!-- 动态生成选项 -->
|
||||
</select>
|
||||
<input id="music-filename" type="text" placeholder="请输入保存为的文件名称(如:周杰伦七里香)" style="display: none;"></input>
|
||||
<div class="component-button-group">
|
||||
<button onclick="confirmSearch()">确定</button>
|
||||
<button onclick="toggleSearch()">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div class="rows">
|
||||
<label for="music_list">播放列表:</label>
|
||||
<select id="music_list"></select>
|
||||
<label for="music_name">歌曲:</label>
|
||||
<select id="music_name"></select>
|
||||
<div>
|
||||
<button id="play_music_list">播放选中歌曲</button>
|
||||
<button id="del_music">删除选中歌曲</button>
|
||||
<button id="web_play">网页播放</button>
|
||||
</div>
|
||||
<div class="play_pannel">
|
||||
<audio autoplay controls src=""></audio>
|
||||
</div>
|
||||
<!-- 定时关机组件 -->
|
||||
<div class="component" id="timer-component">
|
||||
<h2>定时关机</h2>
|
||||
|
||||
<button onclick="timedShutDown('10分钟后关机')">10分钟后关机</button>
|
||||
<button onclick="timedShutDown('30分钟后关机')">30分钟后关机</button>
|
||||
<button onclick="timedShutDown('60分钟后关机')">60分钟后关机</button>
|
||||
<span class="tooltip timer-tooltip" style="display: none;">已发送指令</span>
|
||||
<div class="component-button-one">
|
||||
<button onclick="toggleTimer()">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div class="rows">
|
||||
<input id="music-url" type="text" value="https://lhttp.qtfm.cn/live/4915/64k.mp3"></input>
|
||||
<button id="playurl">播放链接</button>
|
||||
<!-- 播放链接组件 -->
|
||||
<div class="component" id="playlink-component">
|
||||
<h2>播放链接</h2>
|
||||
<input type="text" id="music-url" class="search-input" placeholder="请输入播放链接"
|
||||
value="https://lhttp.qtfm.cn/live/4915/64k.mp3">
|
||||
<div class="component-button-group">
|
||||
<button id="playurl">播放链接</button>
|
||||
<button onclick="togglePlayLink()">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
<!-- 音量组件 -->
|
||||
<div class="component" id="volume-component">
|
||||
<h2>调节音量</h2>
|
||||
<input type="range" id="volume" class="volume-slider" />
|
||||
<div class="component-button-one">
|
||||
<button onclick="toggleVolume()">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 删除确认组件 -->
|
||||
<div class="component" id="delete-component">
|
||||
<h2>警告</h2>
|
||||
<p>你确定要删除歌曲 <span id="delete-music-name"></span> 吗?</p>
|
||||
<p style="font-weight: bold;">注意:该操作会永久删除该歌曲且不可撤销</p>
|
||||
<div class="component-button-group">
|
||||
<button onclick="confirmDelete()">确定</button>
|
||||
<button onclick="toggleDelete()">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 警告组件 -->
|
||||
<div class="component" id="warning-component">
|
||||
<h2>警告</h2>
|
||||
<p>当前页面的HOST与设置中的HOST不一致,请检查是否设置错误</p>
|
||||
<p>当前HOST: <span id="local-host"></span></p>
|
||||
<p>设置中的HOST: <span id="setting-host"></span></p>
|
||||
<div class="component-button-group">
|
||||
<a href="./setting.html" target="_blank"><button>立即修改</button></a>
|
||||
<button onclick="nowarning()">继续并不再显示</button>
|
||||
<button onclick="toggleWarning()">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 更新组件 -->
|
||||
<div class="component" id="update-component" style="display: none;">
|
||||
<h2>更新</h2>
|
||||
<label for="update-version" style="display: flex;align-items: center;">选择版本:
|
||||
<a class="option-inline changelog-button" href="https://xdocs.hanxi.cc/issues/changelog.html" target="_blank">
|
||||
<span class="material-icons">list_alt</span>
|
||||
<span>版本日志</span>
|
||||
</a>
|
||||
</label>
|
||||
|
||||
<select id="update-version" class="version-selector">
|
||||
<option value="test" selected>test</option>
|
||||
</select>
|
||||
|
||||
<label for="lite">选择类型:</label>
|
||||
<select id="lite" class="version-selector">
|
||||
<option value="true" selected>轻量(不更新ffmpeg)</option>
|
||||
<option value="false">完整(更新ffmpeg)</option>
|
||||
</select>
|
||||
|
||||
<div class="component-button-group">
|
||||
<button onclick="doUpdates()">更新</button>
|
||||
<button onclick="toggleUpdate()">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
Powered by XiaoMusic
|
||||
</div>
|
||||
|
||||
<script src="./md.js?version=1735438766">
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>M3U to JSON Converter</title>
|
||||
<link rel="stylesheet" type="text/css" href="./style.css?version=1732797036">
|
||||
<link rel="stylesheet" type="text/css" href="./main.css?version=1735438766">
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>
|
||||
@@ -16,6 +16,9 @@
|
||||
gtag('config', 'G-Z09NC1K7ZW');
|
||||
</script>
|
||||
|
||||
<!-- umami -->
|
||||
<script defer src="https://umami.hanxi.cc/script.js" data-website-id="7bfb0890-4115-4260-8892-b391513e7e99"></script>
|
||||
|
||||
<!--
|
||||
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
|
||||
<script>
|
||||
@@ -69,7 +72,7 @@ function convertToJSON() {
|
||||
<textarea id="json-output" rows="10" cols="50" placeholder="转换后的JSON..."></textarea>
|
||||
</body>
|
||||
<footer>
|
||||
<p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p>
|
||||
<p>Powered by <a href="https://xdocs.hanxi.cc" target="_blank">XiaoMusic</a></p>
|
||||
</footer>
|
||||
</html>
|
||||
|
||||
|
||||
353
xiaomusic/static/default/main.css
Normal file
353
xiaomusic/static/default/main.css
Normal file
@@ -0,0 +1,353 @@
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f0f0f0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.index_page {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.player {
|
||||
background-color: #ffffff;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||
text-align: center;
|
||||
max-width: 360px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
h1 a {
|
||||
margin-left: 10px;
|
||||
font-size: 14px;
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
h1 a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.new-badge {
|
||||
background-color: #ff4757;
|
||||
color: white;
|
||||
border-radius: 12px;
|
||||
padding: 2px 6px;
|
||||
font-size: 12px;
|
||||
margin-left: 5px;
|
||||
display: none;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
margin: 5px 0;
|
||||
display: block;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
select,
|
||||
input[type="range"],
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
input[type="number"] {
|
||||
padding: 10px;
|
||||
border: 1px solid #cccccc;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
transition: border 0.2s ease;
|
||||
box-sizing: border-box
|
||||
}
|
||||
|
||||
select:focus,
|
||||
input[type="text"]:focus {
|
||||
border-color: #007bff;
|
||||
outline: none;
|
||||
}
|
||||
select {
|
||||
overflow: hidden;
|
||||
}
|
||||
button {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 10px 15px;
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
font-size: 16px;
|
||||
transition: background-color 0.3s;
|
||||
position: relative; /* 为tooltip绝对定位做准备 */
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
.tooltip {
|
||||
visibility: hidden;
|
||||
background-color: #555;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
border-radius: 6px;
|
||||
padding: 5px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
bottom: 150%; /* 控制tooltip相对按钮的位置 */
|
||||
left: 50%;
|
||||
margin-left: -50px; /* 调整位置以居中显示 Tooltip */
|
||||
width: 80px; /* 设置固定宽度以保证足够的显示空间 */
|
||||
white-space: nowrap; /* 防止换行 */
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
button:hover .tooltip,.option-inline:hover .tooltip,.control-button:hover .tooltip {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
.option-inline{
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
}
|
||||
.option-inline:hover .tooltip {
|
||||
/* 位置调整到左边 */
|
||||
left: -50px; /* 调整位置以居中显示 Tooltip */
|
||||
bottom: 0;
|
||||
}
|
||||
.control-button{
|
||||
position: relative; /* 为tooltip绝对定位做准备 */
|
||||
}
|
||||
.progress {
|
||||
width: 90%;
|
||||
height: 6px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 6px;
|
||||
margin: 15px 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
progress::-webkit-progress-bar {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
progress::-moz-progress-bar,
|
||||
progress::-webkit-progress-value {
|
||||
background: #007bff;
|
||||
}
|
||||
|
||||
.current-song {
|
||||
color: #333;
|
||||
margin: 10px 0;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.component {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: #ffffff;
|
||||
padding: 5px 15px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
|
||||
z-index: 100;
|
||||
width: 300px;
|
||||
}
|
||||
.component input {
|
||||
width: 90%;
|
||||
}
|
||||
.component-button-group {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.component-button-one {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
#warning-component p span {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.player-controls {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.play {
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.timer-tooltip {
|
||||
bottom: 50%;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
.favorite.favorite-active .material-icons {
|
||||
color: #ff6347;
|
||||
}
|
||||
#audio {
|
||||
width: 100%;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.qrcode {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.login-tips {
|
||||
color: red;
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.login-tips a {
|
||||
color: rgb(9, 105, 218);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* setting.html */
|
||||
.rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.rows a {
|
||||
color: rgb(9, 105, 218);
|
||||
text-decoration: none;
|
||||
}
|
||||
.rows a:hover {
|
||||
color: rgb(9, 95, 198);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
textarea {
|
||||
margin-left: 5%;
|
||||
margin-right: 5%;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
width: 90%;
|
||||
max-width: 400px;
|
||||
height: 200px;
|
||||
}
|
||||
.custom-checkbox {
|
||||
display: inline-block;
|
||||
margin: 10px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
vertical-align: middle; /* 确保与标签垂直居中对齐 */
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: inline-block;
|
||||
width: 180px;
|
||||
background-color: #fff;
|
||||
border: 0px solid #ccc;
|
||||
border-radius: 3px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
vertical-align: middle; /* 确保与复选框垂直居中对齐 */
|
||||
margin-left: 1px; /* 给复选框和标签之间一些距离,如果需要的话 */
|
||||
padding: 5px 10px;
|
||||
}
|
||||
.debug {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 440px) {
|
||||
.player{
|
||||
width: 90%;
|
||||
}
|
||||
.footer {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.mini-button {
|
||||
padding: 0px 0px;
|
||||
margin: 0px;
|
||||
font-size: 14px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.playlist-selector,
|
||||
.device-selector,
|
||||
.version-selector,
|
||||
.song-selector {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.mode-controls {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
border-top: 1px solid #ddd;
|
||||
color: #555;
|
||||
}
|
||||
.icon-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.icon-item p {
|
||||
font-size: 12px;
|
||||
margin: 5px 0 0;
|
||||
}
|
||||
.disabled {
|
||||
color: #ccc;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
span,p {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.changelog-button {
|
||||
padding: 0px 0px;
|
||||
margin: 0px;
|
||||
font-size: 14px;
|
||||
margin-left: auto;
|
||||
text-decoration: none;
|
||||
}
|
||||
713
xiaomusic/static/default/md.js
Normal file
713
xiaomusic/static/default/md.js
Normal file
@@ -0,0 +1,713 @@
|
||||
// $(function () {
|
||||
|
||||
// })
|
||||
let isPlaying = false;
|
||||
let playModeIndex = 2;
|
||||
//重新设计playModes
|
||||
const playModes = {
|
||||
0: {
|
||||
icon: "repeat_one",
|
||||
cmd: "单曲循环",
|
||||
},
|
||||
1: {
|
||||
icon: "repeat",
|
||||
cmd: "全部循环",
|
||||
},
|
||||
2: {
|
||||
icon: "shuffle",
|
||||
cmd: "随机播放",
|
||||
},
|
||||
3: {
|
||||
icon: "filter_1",
|
||||
cmd: "单曲播放",
|
||||
},
|
||||
4: {
|
||||
icon: "playlist_play",
|
||||
cmd: "顺序播放",
|
||||
},
|
||||
};
|
||||
|
||||
let favoritelist = []; //收藏列表
|
||||
|
||||
function webPlay() {
|
||||
console.log("webPlay");
|
||||
const music_name = $("#music_name").val();
|
||||
$.get(`/musicinfo?name=${music_name}`, function (data, status) {
|
||||
console.log(data);
|
||||
if (data.ret == "OK") {
|
||||
validHost(data.url) && $("audio").attr("src", data.url);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function play() {
|
||||
var did = $("#did").val();
|
||||
if (did == "web_device") {
|
||||
webPlay();
|
||||
} else {
|
||||
playOnDevice();
|
||||
}
|
||||
}
|
||||
|
||||
function playOnDevice() {
|
||||
console.log("playOnDevice");
|
||||
var music_list = $("#music_list").val();
|
||||
var music_name = $("#music_name").val();
|
||||
if (no_warning) {
|
||||
do_play_music_list(music_list, music_name);
|
||||
return;
|
||||
}
|
||||
$.get(`/musicinfo?name=${music_name}`, function (data, status) {
|
||||
console.log(data);
|
||||
if (data.ret == "OK") {
|
||||
console.log(
|
||||
"%cmd.js:42 validHost(data.url) ",
|
||||
"color: #007acc;",
|
||||
validHost(data.url)
|
||||
);
|
||||
validHost(data.url) && do_play_music_list(music_list, music_name);
|
||||
}
|
||||
});
|
||||
}
|
||||
function stopPlay() {
|
||||
sendcmd("关机");
|
||||
}
|
||||
|
||||
function prevTrack() {
|
||||
sendcmd("上一首");
|
||||
}
|
||||
|
||||
function nextTrack() {
|
||||
sendcmd("下一首");
|
||||
}
|
||||
|
||||
function togglePlayMode(isSend = true) {
|
||||
const modeBtnIcon = $("#modeBtn .material-icons");
|
||||
if (playModeIndex == '') {
|
||||
playModeIndex = 2;
|
||||
}
|
||||
modeBtnIcon.text(playModes[playModeIndex].icon);
|
||||
$("#modeBtn .tooltip").text(playModes[playModeIndex].cmd);
|
||||
// return;
|
||||
isSend && sendcmd(playModes[playModeIndex].cmd);
|
||||
console.log(`当前播放模式: ${playModeIndex} ${playModes[playModeIndex].cmd}`);
|
||||
playModeIndex = (playModeIndex + 1) % Object.keys(playModes).length;
|
||||
}
|
||||
|
||||
function addToFavorites() {
|
||||
const isLiked = $(".favorite").hasClass("favorite-active");
|
||||
const cmd = isLiked ? "取消收藏" : "加入收藏";
|
||||
if (isLiked) {
|
||||
$(".favorite").removeClass("favorite-active");
|
||||
// 取消收藏
|
||||
favoritelist = favoritelist.filter((item) => item != $("#music_name").val());
|
||||
} else {
|
||||
$(".favorite").addClass("favorite-active");
|
||||
// 加入收藏
|
||||
favoritelist.push($("#music_name").val());
|
||||
}
|
||||
sendcmd(cmd);
|
||||
}
|
||||
|
||||
function openSettings() {
|
||||
console.log("打开设置");
|
||||
//新建标签页打开setting.html页面
|
||||
window.open("setting.html", "_blank");
|
||||
}
|
||||
function toggleVolume() {
|
||||
$("#volume-component").toggle();
|
||||
}
|
||||
|
||||
function toggleSearch() {
|
||||
$("#search-component").toggle();
|
||||
}
|
||||
function toggleTimer() {
|
||||
$("#timer-component").toggle();
|
||||
}
|
||||
function togglePlayLink() {
|
||||
$("#playlink-component").toggle(); // 切换播放链接的显示状态
|
||||
}
|
||||
function toggleLocalPlay() {
|
||||
$("#audio").fadeIn();
|
||||
}
|
||||
function toggleWarning() {
|
||||
$("#warning-component").toggle(); // 切换警告框的显示状态
|
||||
}
|
||||
function toggleDelete() {
|
||||
var del_music_name = $("#music_name").val();
|
||||
$("#delete-music-name").text(del_music_name);
|
||||
$("#delete-component").toggle(); // 切换删除框的显示状态
|
||||
}
|
||||
function confirmDelete() {
|
||||
var del_music_name = $("#music_name").val();
|
||||
console.log(`删除歌曲 ${del_music_name}`);
|
||||
$("#delete-component").hide(); // 隐藏删除框
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/delmusic",
|
||||
data: JSON.stringify({ name: del_music_name }),
|
||||
contentType: "application/json; charset=utf-8",
|
||||
success: () => {
|
||||
alert(`删除 ${del_music_name} 成功`);
|
||||
refresh_music_list();
|
||||
},
|
||||
error: () => {
|
||||
alert(`删除 ${del_music_name} 失败`);
|
||||
},
|
||||
});
|
||||
}
|
||||
function formatTime(seconds) {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return `${minutes}:${secs < 10 ? "0" : ""}${secs}`; // Format time as mm:ss
|
||||
}
|
||||
|
||||
var offset = 0;
|
||||
var duration = 0;
|
||||
let no_warning = localStorage.getItem("no-warning");
|
||||
// 拉取现有配置
|
||||
$.get("/getsetting", function (data, status) {
|
||||
console.log(data, status);
|
||||
localStorage.setItem("mi_did", data.mi_did);
|
||||
|
||||
var did = localStorage.getItem("cur_did");
|
||||
var dids = [];
|
||||
if (data.mi_did != null) {
|
||||
dids = data.mi_did.split(",");
|
||||
}
|
||||
console.log("cur_did", did);
|
||||
console.log("dids", dids);
|
||||
if (did != "web_device" && dids.length > 0 && (did == null || did == "" || !dids.includes(did))) {
|
||||
did = dids[0];
|
||||
localStorage.setItem("cur_did", did);
|
||||
}
|
||||
|
||||
window.did = did;
|
||||
$.get(`/getvolume?did=${did}`, function (data, status) {
|
||||
console.log(data, status, data["volume"]);
|
||||
$("#volume").val(data.volume);
|
||||
});
|
||||
refresh_music_list();
|
||||
|
||||
$("#did").empty();
|
||||
var dids = data.mi_did.split(",");
|
||||
$.each(dids, function (index, value) {
|
||||
var cur_device = Object.values(data.devices).find(
|
||||
(device) => device.did === value
|
||||
);
|
||||
if (cur_device) {
|
||||
var option = $("<option></option>")
|
||||
.val(value)
|
||||
.text(cur_device.name)
|
||||
.prop("selected", value === did);
|
||||
$("#did").append(option);
|
||||
|
||||
if (value === did) {
|
||||
playModeIndex = cur_device.play_type;
|
||||
console.log(
|
||||
"%c当前设备播放模式: ",
|
||||
"color: #007acc;",
|
||||
cur_device.play_type
|
||||
);
|
||||
togglePlayMode(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
var option = $("<option></option>")
|
||||
.val("web_device")
|
||||
.text("本机")
|
||||
.prop("selected", "web_device" === did);
|
||||
$("#did").append(option);
|
||||
|
||||
console.log("cur_did", did);
|
||||
$("#did").change(function () {
|
||||
did = $(this).val();
|
||||
localStorage.setItem("cur_did", did);
|
||||
window.did = did;
|
||||
console.log("cur_did", did);
|
||||
location.reload();
|
||||
});
|
||||
|
||||
if (did == "web_device") {
|
||||
$("#audio").fadeIn();
|
||||
$("#device-audio").fadeOut();
|
||||
$(".device-enable").addClass('disabled');
|
||||
} else {
|
||||
$("#audio").fadeOut();
|
||||
$("#device-audio").fadeIn();
|
||||
$(".device-enable").removeClass('disabled');
|
||||
}
|
||||
});
|
||||
|
||||
function compareVersion(version1, version2) {
|
||||
const v1 = version1.split(".").map(Number);
|
||||
const v2 = version2.split(".").map(Number);
|
||||
const len = Math.max(v1.length, v2.length);
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
const num1 = v1[i] || 0;
|
||||
const num2 = v2[i] || 0;
|
||||
if (num1 > num2) return 1;
|
||||
if (num1 < num2) return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 拉取版本
|
||||
$.get("/getversion", function (data, status) {
|
||||
console.log(data, status, data["version"]);
|
||||
$("#version").text(`${data.version}`);
|
||||
|
||||
$.get("/latestversion", function (ret, status) {
|
||||
console.log(ret, status);
|
||||
if (ret.ret == "OK") {
|
||||
const result = compareVersion(ret.version, data.version);
|
||||
if (result > 0) {
|
||||
console.log(`${ret.version} is greater than ${data.version}`);
|
||||
$("#versionnew").text("new").css("display", "inline-block");
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function _refresh_music_list(callback) {
|
||||
$("#music_list").empty();
|
||||
$.get("/musiclist", function (data, status) {
|
||||
console.log(data, status);
|
||||
favoritelist = data["收藏"];
|
||||
$.each(data, function (key, value) {
|
||||
let cnt = value.length;
|
||||
$("#music_list").append(
|
||||
$("<option></option>").val(key).text(`${key} (${cnt})`)
|
||||
);
|
||||
});
|
||||
|
||||
$("#music_list").change(function () {
|
||||
const selectedValue = $(this).val();
|
||||
localStorage.setItem("cur_playlist", selectedValue);
|
||||
$("#music_name").empty();
|
||||
const cur_music = localStorage.getItem("cur_music");
|
||||
console.log("#music_name cur_music", cur_music);
|
||||
$.each(data[selectedValue], function (index, item) {
|
||||
$("#music_name").append($("<option></option>").val(item).text(item).prop("selected", item == cur_music));
|
||||
});
|
||||
});
|
||||
|
||||
$("#music_list").trigger("change");
|
||||
|
||||
// 获取当前播放列表
|
||||
$.get(`/curplaylist?did=${did}`, function (playlist, status) {
|
||||
if (playlist != "") {
|
||||
$("#music_list").val(playlist);
|
||||
$("#music_list").trigger("change");
|
||||
} else {
|
||||
// 使用本地记录的
|
||||
playlist = localStorage.getItem("cur_playlist");
|
||||
if (data.hasOwnProperty(playlist)) {
|
||||
$("#music_list").val(playlist);
|
||||
$("#music_list").trigger("change");
|
||||
}
|
||||
}
|
||||
});
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
// 拉取播放列表
|
||||
function refresh_music_list() {
|
||||
// 刷新列表时清空并临时禁用搜索框
|
||||
const searchInput = document.getElementById("search");
|
||||
const oriPlaceHolder = searchInput.placeholder;
|
||||
const oriValue = searchInput.value;
|
||||
const inputEvent = new Event("input", { bubbles: true });
|
||||
searchInput.value = "";
|
||||
// 分发事件,让其他控件改变状态
|
||||
searchInput.dispatchEvent(inputEvent);
|
||||
searchInput.disabled = true;
|
||||
searchInput.placeholder = "请等待...";
|
||||
|
||||
_refresh_music_list(() => {
|
||||
// 刷新完成再启用
|
||||
searchInput.disabled = false;
|
||||
searchInput.value = oriValue;
|
||||
searchInput.dispatchEvent(inputEvent);
|
||||
searchInput.placeholder = oriPlaceHolder;
|
||||
// 每3秒获取下正在播放的音乐
|
||||
get_playing_music();
|
||||
setInterval(() => {
|
||||
get_playing_music();
|
||||
}, 3000);
|
||||
});
|
||||
}
|
||||
|
||||
function do_play_music_list(listname, musicname) {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/playmusiclist",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({
|
||||
did: did,
|
||||
listname: listname,
|
||||
musicname: musicname,
|
||||
}),
|
||||
success: () => {
|
||||
console.log("do_play_music_list succ", listname, musicname);
|
||||
},
|
||||
error: () => {
|
||||
console.log("do_play_music_list failed", listname, musicname);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
$("#play_music_list").on("click", () => {
|
||||
var music_list = $("#music_list").val();
|
||||
var music_name = $("#music_name").val();
|
||||
if (no_warning) {
|
||||
do_play_music_list(music_list, music_name);
|
||||
return;
|
||||
}
|
||||
$.get(`/musicinfo?name=${music_name}`, function (data, status) {
|
||||
console.log(data);
|
||||
if (data.ret == "OK") {
|
||||
validHost(data.url) && do_play_music_list(music_list, music_name);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#playurl").on("click", () => {
|
||||
var url = $("#music-url").val();
|
||||
const encoded_url = encodeURIComponent(url);
|
||||
$.get(`/playurl?url=${encoded_url}&did=${did}`, function (data, status) {
|
||||
console.log(data);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
function do_play_music(musicname, searchkey) {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/playmusic",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({
|
||||
did: did,
|
||||
musicname: musicname,
|
||||
searchkey: searchkey,
|
||||
}),
|
||||
success: () => {
|
||||
console.log("do_play_music succ", musicname, searchkey);
|
||||
},
|
||||
error: () => {
|
||||
console.log("do_play_music failed", musicname, searchkey);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
$("#play").on("click", () => {
|
||||
var search_key = $("#music-name").val();
|
||||
if (search_key == null) {
|
||||
search_key = "";
|
||||
}
|
||||
var filename = $("#music-filename").val();
|
||||
if (filename == null || filename == "") {
|
||||
filename = search_key;
|
||||
}
|
||||
do_play_music(filename, search_key);
|
||||
});
|
||||
|
||||
$("#volume").on("change", function () {
|
||||
var value = $(this).val();
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/setvolume",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({ did: did, volume: value }),
|
||||
success: () => { },
|
||||
error: () => { },
|
||||
});
|
||||
});
|
||||
|
||||
function check_status_refresh_music_list(retries) {
|
||||
$.get("/cmdstatus", function (data) {
|
||||
if (data.status === "finish") {
|
||||
refresh_music_list();
|
||||
} else if (retries > 0) {
|
||||
setTimeout(function () {
|
||||
check_status_refresh_music_list(retries - 1);
|
||||
}, 1000); // 等待1秒后重试
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function sendcmd(cmd) {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/cmd",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({ did: did, cmd: cmd }),
|
||||
success: () => {
|
||||
if (cmd == "刷新列表") {
|
||||
check_status_refresh_music_list(3); // 最多重试3次
|
||||
}
|
||||
if (
|
||||
["全部循环", "单曲循环", "随机播放", "单曲播放", "顺序播放"].includes(
|
||||
cmd
|
||||
)
|
||||
) {
|
||||
location.reload();
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
// 请求失败时执行的操作
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 监听输入框的输入事件
|
||||
function debounce(func, delay) {
|
||||
let timeout;
|
||||
return function (...args) {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func.apply(this, args), delay);
|
||||
};
|
||||
}
|
||||
function handleSearch() {
|
||||
const searchInput = document.getElementById("search");
|
||||
const musicSelect = document.getElementById("music-name");
|
||||
const musicSelectLabel = document.getElementById("music-name-label");
|
||||
|
||||
searchInput.addEventListener(
|
||||
"input",
|
||||
debounce(function () {
|
||||
const query = searchInput.value.trim();
|
||||
|
||||
if (query.length === 0) {
|
||||
musicSelect.innerHTML = "";
|
||||
musicSelect.style.display = "none";
|
||||
musicSelectLabel.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
musicSelect.style.display = "block";
|
||||
musicSelectLabel.style.display = "block";
|
||||
fetch(`/searchmusic?name=${encodeURIComponent(query)}`)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
musicSelect.innerHTML = ""; // 清空现有选项
|
||||
|
||||
// 找到的优先显示
|
||||
if (data.length > 0) {
|
||||
data.forEach((song) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = song;
|
||||
option.textContent = song;
|
||||
musicSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// 添加用户输入作为一个选项
|
||||
const userOption = document.createElement("option");
|
||||
userOption.value = query;
|
||||
userOption.textContent = `使用关键词播放: ${query}`;
|
||||
musicSelect.appendChild(userOption);
|
||||
|
||||
// 提示没找到
|
||||
if (data.length === 0) {
|
||||
const option = document.createElement("option");
|
||||
option.textContent = "没有匹配的结果";
|
||||
option.disabled = true;
|
||||
musicSelect.appendChild(option);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error fetching data:", error);
|
||||
});
|
||||
}, 600)
|
||||
);
|
||||
|
||||
// 动态显示保存文件名输入框
|
||||
const musicNameSelect = document.getElementById("music-name");
|
||||
const musicFilenameInput = document.getElementById("music-filename");
|
||||
function updateInputVisibility() {
|
||||
const selectedOption =
|
||||
musicNameSelect.options[musicNameSelect.selectedIndex];
|
||||
var startsWithKeyword;
|
||||
if (musicNameSelect.options.length === 0) {
|
||||
startsWithKeyword = false;
|
||||
} else {
|
||||
startsWithKeyword = selectedOption.text.startsWith("使用关键词播放:");
|
||||
}
|
||||
|
||||
if (startsWithKeyword) {
|
||||
musicFilenameInput.style.display = "block";
|
||||
musicFilenameInput.placeholder =
|
||||
"请输入保存为的文件名称(默认:" + selectedOption.value + ")";
|
||||
} else {
|
||||
musicFilenameInput.style.display = "none";
|
||||
}
|
||||
}
|
||||
// 观察元素修改
|
||||
const observer = new MutationObserver((mutationsList) => {
|
||||
for (const mutation of mutationsList) {
|
||||
if (mutation.type === "childList") {
|
||||
updateInputVisibility();
|
||||
}
|
||||
}
|
||||
});
|
||||
observer.observe(musicNameSelect, { childList: true });
|
||||
// 监听用户输入
|
||||
musicNameSelect.addEventListener("change", updateInputVisibility);
|
||||
}
|
||||
|
||||
handleSearch();
|
||||
|
||||
function get_playing_music() {
|
||||
$.get(`/playingmusic?did=${did}`, function (data, status) {
|
||||
console.log(data);
|
||||
if (data.ret == "OK") {
|
||||
if (data.is_playing) {
|
||||
$("#playering-music").text(`【播放中】 ${data.cur_music}`);
|
||||
} else {
|
||||
$("#playering-music").text(`【空闲中】 ${data.cur_music}`);
|
||||
}
|
||||
offset = data.offset;
|
||||
duration = data.duration;
|
||||
//检查歌曲是否在收藏中,如果是,设置收藏按钮为选中状态
|
||||
console.log(
|
||||
"%cmd.js:614 object",
|
||||
"color: #007acc;",
|
||||
favoritelist.includes(data.cur_music)
|
||||
);
|
||||
if (favoritelist.includes(data.cur_music)) {
|
||||
$(".favorite").addClass("favorite-active");
|
||||
} else {
|
||||
$(".favorite").removeClass("favorite-active");
|
||||
}
|
||||
localStorage.setItem("cur_music", data.cur_music);
|
||||
}
|
||||
});
|
||||
}
|
||||
setInterval(() => {
|
||||
if (duration > 0) {
|
||||
offset++;
|
||||
$("#progress").val((offset / duration) * 100);
|
||||
$("#current-time").text(formatTime(offset));
|
||||
$("#duration").text(formatTime(duration));
|
||||
} else {
|
||||
$("#current-time").text(formatTime(0));
|
||||
$("#duration").text(formatTime(0));
|
||||
}
|
||||
}, 1000);
|
||||
function formatTime(seconds) {
|
||||
var minutes = Math.floor(seconds / 60);
|
||||
var remainingSeconds = Math.floor(seconds % 60);
|
||||
return `${minutes.toString().padStart(2, "0")}:${remainingSeconds
|
||||
.toString()
|
||||
.padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
$("audio").on("error", (e) => {
|
||||
//如果audio标签的src为空,则不做任何操作,兼容安卓端的低版本webview
|
||||
if ($("audio").attr("src") === "") {
|
||||
return;
|
||||
}
|
||||
console.log(
|
||||
"%c网页播放出现错误: ",
|
||||
"color: #007acc;",
|
||||
e.currentTarget.error.code,
|
||||
e.currentTarget.error.message
|
||||
);
|
||||
alert(
|
||||
e.currentTarget.error.code == 4
|
||||
? "无法打开媒体文件,XIAOMUSIC_HOSTNAME或端口地址错误,请重新设置"
|
||||
: "在线播放失败,请截图反馈: " + e.currentTarget.error.message
|
||||
);
|
||||
});
|
||||
function validHost(url) {
|
||||
//如果 localStorage 中有 no-warning 则直接返回true
|
||||
if (no_warning) {
|
||||
return true;
|
||||
}
|
||||
const local = location.host;
|
||||
const host = new URL(url).host;
|
||||
// 如果当前页面的Host与设置中的XIAOMUSIC_HOSTNAME、PORT一致, 不再提醒
|
||||
if (local === host) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$("#local-host").text(local);
|
||||
$("#setting-host").text(host);
|
||||
$("#warning-component").show();
|
||||
console.log("%c 验证返回false", "color: #007acc;");
|
||||
return false;
|
||||
}
|
||||
|
||||
function nowarning() {
|
||||
localStorage.setItem("no-warning", "true");
|
||||
no_warning = true;
|
||||
$("#warning-component").hide();
|
||||
}
|
||||
function timedShutDown(cmd) {
|
||||
$(".timer-tooltip").toggle();
|
||||
sendcmd(cmd);
|
||||
setTimeout(() => {
|
||||
$(".timer-tooltip").fadeOut();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 绑定点击事件,显示弹窗
|
||||
$('#version').on('click', function () {
|
||||
$.get("https://xdocs.hanxi.cc/versions.json", function (data, status) {
|
||||
console.log(data);
|
||||
const versionSelect = document.getElementById("update-version");
|
||||
versionSelect.innerHTML = "";
|
||||
data.forEach((item) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = item.version;
|
||||
option.textContent = item.version;
|
||||
versionSelect.appendChild(option);
|
||||
});
|
||||
});
|
||||
$('#update-component').show();
|
||||
});
|
||||
|
||||
// 关闭更新弹窗
|
||||
function toggleUpdate() {
|
||||
$('#update-component').hide();
|
||||
}
|
||||
|
||||
function doUpdates() {
|
||||
const version = $("#update-version").val();
|
||||
let lite = $("#lite").val();
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: `/updateversion?version=${version}&lite=${lite}`,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
success: (data) => {
|
||||
if (data.ret == "OK") {
|
||||
alert(`更新成功,请刷新页面`);
|
||||
location.reload();
|
||||
} else {
|
||||
alert(`更新失败: ${data.ret}`);
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
alert(`更新失败`);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function confirmSearch() {
|
||||
var search_key = $("#search").val();
|
||||
if (search_key == null) {
|
||||
search_key = "";
|
||||
}
|
||||
var filename = $("#music-name").val();
|
||||
var musicfilename = $("#music-filename").val();
|
||||
if ((filename == null || filename == "" || filename == search_key)
|
||||
&& (musicfilename != null && musicfilename != "")) {
|
||||
filename = musicfilename;
|
||||
}
|
||||
console.log("confirmSearch", filename, search_key);
|
||||
do_play_music(filename, search_key);
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>小爱音箱操控面板</title>
|
||||
<script src="./jquery-3.7.1.min.js?version=1732797036"></script>
|
||||
<script src="./setting.js?version=1732797036"></script>
|
||||
<link rel="stylesheet" type="text/css" href="./style.css?version=1732797036">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
<script src="./jquery-3.7.1.min.js?version=1735438766"></script>
|
||||
<script src="./setting.js?version=1735438766"></script>
|
||||
<link rel="stylesheet" type="text/css" href="./main.css?version=1735438766">
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>
|
||||
@@ -17,6 +18,9 @@
|
||||
gtag('config', 'G-Z09NC1K7ZW');
|
||||
</script>
|
||||
|
||||
<!-- umami -->
|
||||
<script defer src="https://umami.hanxi.cc/script.js" data-website-id="7bfb0890-4115-4260-8892-b391513e7e99"></script>
|
||||
|
||||
<!--
|
||||
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
|
||||
<script>
|
||||
@@ -27,9 +31,7 @@ var vConsole = new window.VConsole();
|
||||
</head>
|
||||
<body>
|
||||
<h2>小爱音箱设置面板
|
||||
(<a id="version" href="https://github.com/hanxi/xiaomusic/blob/main/CHANGELOG.md">
|
||||
版本未知
|
||||
</a>)
|
||||
(<a id="version" href="https://xdocs.hanxi.cc/issues/changelog.html">版本未知</a>)
|
||||
</h2>
|
||||
<hr>
|
||||
|
||||
@@ -43,54 +45,61 @@ var vConsole = new window.VConsole();
|
||||
<div id="setting">
|
||||
<div class="rows">
|
||||
<label for="account">*小米账号:</label>
|
||||
<input id="account" type="text" placeholder="填写小米登录账号"></input>
|
||||
<input id="account" type="text" placeholder="填写小米登录账号" />
|
||||
|
||||
<label for="password">*小米密码:</label>
|
||||
<input id="password" type="password" placeholder="填写小米登录密码"></input>
|
||||
<input id="password" type="password" placeholder="填写小米登录密码" />
|
||||
|
||||
<label for="hostname">*XIAOMUSIC_HOSTNAME(IP或域名):</label>
|
||||
<input id="hostname" type="text"></input>
|
||||
<label for="hostname">*XIAOMUSIC_HOSTNAME(NAS的IP或域名):</label>
|
||||
<input id="hostname" type="text" />
|
||||
|
||||
<label for="public_port">*本地端口(0表示跟容器端口一致):</label>
|
||||
<input id="public_port" type="number" value="0" />
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<div class="rows">
|
||||
<label for="verbose">是否开启调试日志:</label>
|
||||
<select id="verbose">
|
||||
<option value="true" selected>true</option>
|
||||
<option value="false">false</option>
|
||||
<option value="true">true</option>
|
||||
<option value="false" selected>false</option>
|
||||
</select>
|
||||
|
||||
<label for="group_list">设备分组配置:<a href="https://github.com/hanxi/xiaomusic/issues/65#issuecomment-2215736529" target="_blank">文档</a></label>
|
||||
<input id="group_list" type="text" placeholder="did1:组名1,did2:组名1,did3:组名2"></input>
|
||||
<input id="group_list" type="text" placeholder="did1:组名1,did2:组名1,did3:组名2" />
|
||||
|
||||
<label for="music_path">音乐目录:</label>
|
||||
<input id="music_path" type="text" value="music"></input>
|
||||
<input id="music_path" type="text" value="music" />
|
||||
|
||||
<label for="download_path">音乐下载目录(必须是music的子目录):</label>
|
||||
<input id="download_path" type="text" value='music/download'></input>
|
||||
<input id="download_path" type="text" value='music/download' />
|
||||
|
||||
<label for="conf_path">配置文件目录:</label>
|
||||
<input id="conf_path" type="text"></input>
|
||||
<input id="conf_path" type="text" />
|
||||
|
||||
<label for="cache_dir">缓存文件目录:</label>
|
||||
<input id="cache_dir" type="text"></input>
|
||||
<input id="cache_dir" type="text" />
|
||||
|
||||
<label for="temp_path">临时文件目录:</label>
|
||||
<input id="temp_path" type="text" value="music/tmp"></input>
|
||||
<input id="temp_path" type="text" value="music/tmp" />
|
||||
|
||||
<label for="ffmpeg_location">ffmpeg路径:</label>
|
||||
<input id="ffmpeg_location" type="text" value="./ffmpeg/bin"></input>
|
||||
<input id="ffmpeg_location" type="text" value="./ffmpeg/bin" />
|
||||
|
||||
<label for="log_file">日志路径:</label>
|
||||
<input id="log_file" type="text" value="/tmp/xiaomusic.txt"></input>
|
||||
<input id="log_file" type="text" value="xiaomusic.log.txt" />
|
||||
|
||||
<label for="active_cmd">允许唤醒的命令:</label>
|
||||
<input id="active_cmd" type="text" value="play,random_play,playlocal,play_music_list,stop"></input>
|
||||
<input id="active_cmd" type="text" value="play,random_play,playlocal,play_music_list,stop" />
|
||||
|
||||
<label for="exclude_dirs">忽略目录(逗号分割):</label>
|
||||
<input id="exclude_dirs" type="text" value="@eaDir,tmp"></input>
|
||||
<input id="exclude_dirs" type="text" value="@eaDir,tmp" />
|
||||
|
||||
<label for="ignore_tag_dirs">不扫描标签信息目录(逗号分割):</label>
|
||||
<input id="ignore_tag_dirs" type="text" value="" />
|
||||
|
||||
<label for="music_path_depth">目录深度:</label>
|
||||
<input id="music_path_depth" type="number" value="10"></input>
|
||||
<input id="music_path_depth" type="number" value="10" />
|
||||
|
||||
<label for="search_prefix">XIAOMUSIC_SEARCH(歌曲下载方式):</label>
|
||||
<select id="search_prefix">
|
||||
@@ -99,7 +108,7 @@ var vConsole = new window.VConsole();
|
||||
</select>
|
||||
|
||||
<label for="proxy">XIAOMUSIC_PROXY(ytsearch需要):</label>
|
||||
<input id="proxy" type="text" placeholder="http://192.168.2.5:8080"></input>
|
||||
<input id="proxy" type="text" placeholder="http://192.168.2.5:8080" />
|
||||
|
||||
<label for="remove_id3tag">去除MP3 ID3v2和填充:</label>
|
||||
<select id="remove_id3tag">
|
||||
@@ -114,7 +123,7 @@ var vConsole = new window.VConsole();
|
||||
</select>
|
||||
|
||||
<label for="miio_tts_command">MiIO tts 指令(解决部分型号没有提示音的问题):</label>
|
||||
<input id="miio_tts_command" type="text" placeholder="如:5 或者 5-3"></input>
|
||||
<input id="miio_tts_command" type="text" placeholder="如:5 或者 5-3" />
|
||||
|
||||
<label for="disable_httpauth">关闭控制台密码验证:</label>
|
||||
<select id="disable_httpauth">
|
||||
@@ -122,9 +131,9 @@ var vConsole = new window.VConsole();
|
||||
<option value="false">false</option>
|
||||
</select>
|
||||
<label for="httpauth_username">控制台账户:</label>
|
||||
<input id="httpauth_username" type="text" value=""></input>
|
||||
<input id="httpauth_username" type="text" value="" />
|
||||
<label for="httpauth_password">控制台密码:</label>
|
||||
<input id="httpauth_password" type="password" value=""></input>
|
||||
<input id="httpauth_password" type="password" value="" />
|
||||
|
||||
<label for="disable_download">关闭下载功能:</label>
|
||||
<select id="disable_download">
|
||||
@@ -133,12 +142,12 @@ var vConsole = new window.VConsole();
|
||||
</select>
|
||||
|
||||
<label for="use_music_audio_id">触屏版显示歌曲ID:</label>
|
||||
<input id="use_music_audio_id" type="text" value="1582971365183456177"></input>
|
||||
<input id="use_music_audio_id" type="text" value="1582971365183456177" />
|
||||
<label for="use_music_id">触屏版显示歌曲分段ID:</label>
|
||||
<input id="use_music_id" type="text" value="355454500"></input>
|
||||
<input id="use_music_id" type="text" value="355454500" />
|
||||
|
||||
<label for="fuzzy_match_cutoff">模糊匹配阈值(0.1~0.9):</label>
|
||||
<input id="fuzzy_match_cutoff" type="number" value="0.6"></input>
|
||||
<input id="fuzzy_match_cutoff" type="number" value="0.6" />
|
||||
|
||||
<label for="enable_fuzzy_match">开启模糊搜索:</label>
|
||||
<select id="enable_fuzzy_match">
|
||||
@@ -158,39 +167,37 @@ var vConsole = new window.VConsole();
|
||||
<option value="false" selected>false</option>
|
||||
</select>
|
||||
|
||||
<label for="port">监听端口(修改后需要重启):</label>
|
||||
<input id="port" type="number" value="8090"></input>
|
||||
|
||||
<label for="public_port">外网访问端口(0表示跟监听端口一致):</label>
|
||||
<input id="public_port" type="number" value="0"></input>
|
||||
|
||||
<label for="pull_ask_sec">获取对话记录间隔(秒):</label>
|
||||
<input id="pull_ask_sec" type="number" value="1"></input>
|
||||
<input id="pull_ask_sec" type="number" value="1" />
|
||||
|
||||
<label for="delay_sec">下一首歌延迟播放秒数:</label>
|
||||
<input id="delay_sec" type="number" value="3"></input>
|
||||
<input id="delay_sec" type="number" value="3" />
|
||||
|
||||
<label for="stop_tts_msg">停止提示音:</label>
|
||||
<input id="stop_tts_msg" type="text" value="收到,再见"></input>
|
||||
<input id="stop_tts_msg" type="text" value="收到,再见" />
|
||||
<label for="play_type_one_tts_msg">单曲循环提示音:</label>
|
||||
<input id="play_type_one_tts_msg" type="text" value="已经设置为单曲循环"></input>
|
||||
<input id="play_type_one_tts_msg" type="text" value="已经设置为单曲循环" />
|
||||
<label for="play_type_all_tts_msg">全部循环提示音:</label>
|
||||
<input id="play_type_all_tts_msg" type="text" value="已经设置为全部循环"></input>
|
||||
<input id="play_type_all_tts_msg" type="text" value="已经设置为全部循环" />
|
||||
<label for="play_type_rnd_tts_msg">随机播放提示音:</label>
|
||||
<input id="play_type_rnd_tts_msg" type="text" value="已经设置为随机播放"></input>
|
||||
<input id="play_type_rnd_tts_msg" type="text" value="已经设置为随机播放" />
|
||||
<label for="play_type_sin_tts_msg">单曲播放提示音:</label>
|
||||
<input id="play_type_sin_tts_msg" type="text" value="已经设置为单曲播放"></input>
|
||||
<input id="play_type_sin_tts_msg" type="text" value="已经设置为单曲播放" />
|
||||
<label for="play_type_seq_tts_msg">顺序播放提示音:</label>
|
||||
<input id="play_type_seq_tts_msg" type="text" value="已经设置为顺序播放"></input>
|
||||
<input id="play_type_seq_tts_msg" type="text" value="已经设置为顺序播放" />
|
||||
|
||||
<label for="keywords_playlocal">播放本地歌曲口令:</label>
|
||||
<input id="keywords_playlocal" type="text" value="播放本地歌曲,本地播放歌曲"></input>
|
||||
<input id="keywords_playlocal" type="text" value="播放本地歌曲,本地播放歌曲" />
|
||||
<label for="keywords_play">播放歌曲口令:</label>
|
||||
<input id="keywords_play" type="text" value="播放歌曲,放歌曲"></input>
|
||||
<input id="keywords_play" type="text" value="播放歌曲,放歌曲" />
|
||||
<label for="keywords_playlist">播放列表口令:</label>
|
||||
<input id="keywords_playlist" type="text" value="播放列表,播放歌单"></input>
|
||||
<input id="keywords_playlist" type="text" value="播放列表,播放歌单" />
|
||||
<label for="keywords_stop">停止口令:</label>
|
||||
<input id="keywords_stop" type="text" value="关机,暂停,停止,停止播放"></input>
|
||||
<input id="keywords_stop" type="text" value="关机,暂停,停止,停止播放" />
|
||||
<label for="keywords_search_playlocal">本地搜索播放口令(会产生临时播放列表):</label>
|
||||
<input id="keywords_search_playlocal" type="text" value="本地搜索播放" />
|
||||
<label for="keywords_search_play">搜索播放口令(会产生临时播放列表):</label>
|
||||
<input id="keywords_search_play" type="text" value="搜索播放" />
|
||||
|
||||
<label for="enable_yt_dlp_cookies">启用yt-dlp-cookies(需要先上传yt-dlp-cookies.txt文件):</label>
|
||||
<select id="enable_yt_dlp_cookies">
|
||||
@@ -204,8 +211,16 @@ var vConsole = new window.VConsole();
|
||||
<option value="false" selected>false</option>
|
||||
</select>
|
||||
|
||||
<label for="music_list_url">歌单地址:</label>
|
||||
<input id="music_list_url" type="text" value="https://gist.githubusercontent.com/hanxi/dda82d964a28f8110f8fba81c3ff8314/raw/example.json"></input>
|
||||
<label for="recently_added_playlist_len">最近新增的歌曲数量:</label>
|
||||
<input id="recently_added_playlist_len" type="number" value="50" />
|
||||
|
||||
<label for="music_list_url" style="display: flex;align-items: center;">歌单地址:
|
||||
<button class="option-inline mini-button" id="get_music_list">
|
||||
<span class="material-icons">sync_alt</span>
|
||||
<span>获取歌单</span>
|
||||
</button>
|
||||
</label>
|
||||
<input id="music_list_url" type="text" value="https://gist.githubusercontent.com/hanxi/dda82d964a28f8110f8fba81c3ff8314/raw/example.json" />
|
||||
|
||||
<label for="music_list_json">歌单内容:<a href="https://github.com/hanxi/xiaomusic/issues/78" target="_blank">格式文档</a></label>
|
||||
<textarea id="music_list_json" type="text"></textarea>
|
||||
@@ -223,31 +238,36 @@ var vConsole = new window.VConsole();
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<button onclick="location.href='/static/default/index.html';">返回首页</button>
|
||||
<button id="get_music_list">获取歌单</button>
|
||||
<button class="save-button">保存</button>
|
||||
<hr>
|
||||
|
||||
<button id="refresh_music_tag">刷新tag</button>
|
||||
<button id="clear_cache">清空缓存</button>
|
||||
<a class="button" href="/downloadlog" download="xiaomusic.txt">下载日志文件</a>
|
||||
|
||||
<hr>
|
||||
|
||||
<button onclick="location.href='/docs';">查看接口文档</button>
|
||||
<a class="button" href="./m3u.html" target="_blank">m3u文件转换</a>
|
||||
<a class="button" href="./downloadtool.html" target="_blank">歌曲下载工具</a>
|
||||
<hr>
|
||||
|
||||
<a class="button" href="./debug.html" target="_blank">调试工具</a>
|
||||
<a class="button" href="https://afdian.com/a/imhanxi" target="_blank">💰 爱发电</a>
|
||||
<a class="button" href="https://github.com/hanxi/xiaomusic" target="_blank">点个 Star ⭐</a>
|
||||
<div class="buttons">
|
||||
<div class="button-group">
|
||||
<button onclick="location.href='/static/default/index.html';">返回首页</button>
|
||||
<button class="save-button">保存</button>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button id="refresh_music_tag">刷新tag</button>
|
||||
<button id="clear_cache">清空缓存</button>
|
||||
<a href="/downloadlog" download="xiaomusic.txt"><button>下载日志文件</button></a>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<a href="/docs" target="_blank"><button>接口文档</button></a>
|
||||
<a href="./m3u.html" target="_blank"><button>m3u转换</button></a>
|
||||
<a href="./downloadtool.html" target="_blank"><button>歌曲下载工具</button></a>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<a href="./debug.html" target="_blank"><button>调试工具</button></a>
|
||||
<a href="https://afdian.com/a/imhanxi" target="_blank"><button>💰 爱发电</button></a>
|
||||
<a href="https://github.com/hanxi/xiaomusic" target="_blank"><button>点个 Star ⭐</button></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rows">
|
||||
<img class="qrcode" src="./qrcode.png" alt="请涵曦喝奶茶🧋">
|
||||
</div>
|
||||
<footer>
|
||||
<p>Powered by <a href="https://github.com/hanxi/xiaomusic" target="_blank">xiaomusic</a></p>
|
||||
<p>Powered by <a href="https://xdocs.hanxi.cc" target="_blank">XiaoMusic</a></p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -16,13 +16,19 @@ $(function(){
|
||||
});
|
||||
};
|
||||
|
||||
function updateCheckbox(selector, mi_did, device_list) {
|
||||
function updateCheckbox(selector, mi_did, device_list,accountPassValid) {
|
||||
// 清除现有的内容
|
||||
$(selector).empty();
|
||||
|
||||
// 将 mi_did 字符串通过逗号分割转换为数组,以便于判断默认选中项
|
||||
var selected_dids = mi_did.split(',');
|
||||
|
||||
//如果device_list为空,则可能是未设置小米账号密码或者已设置密码,但是没有过小米验证,此处需要提示用户
|
||||
if (device_list.length == 0) {
|
||||
const loginTips = accountPassValid ? `<div class="login-tips">未发现可用的小爱设备,请检查账号密码是否输错,并关闭加速代理或在<a href="https://www.mi.com">小米官网</a>登陆过人脸或滑块验证。如仍未解决。请根据<a href="https://github.com/hanxi/xiaomusic/issues/99">FAQ</a>的内容解决问题。</div>` : `<div class="login-tips">未发现可用的小爱设备,请先在下面的输入框中设置小米的<b>账号、密码</b></div>`;
|
||||
$(selector).append(loginTips);
|
||||
return;
|
||||
}
|
||||
$.each(device_list, function(index, device) {
|
||||
var did = device.miotDID;
|
||||
var hardware = device.hardware;
|
||||
@@ -64,7 +70,8 @@ $(function(){
|
||||
// 拉取现有配置
|
||||
$.get("/getsetting?need_device_list=true", function(data, status) {
|
||||
console.log(data, status);
|
||||
updateCheckbox("#mi_did", data.mi_did, data.device_list);
|
||||
const accountPassValid = data.account && data.password;
|
||||
updateCheckbox("#mi_did", data.mi_did, data.device_list, accountPassValid);
|
||||
|
||||
// 初始化显示
|
||||
for (const key in data) {
|
||||
@@ -184,4 +191,14 @@ $(function(){
|
||||
$("#clear_cache").on("click", () => {
|
||||
localStorage.clear();
|
||||
});
|
||||
$("#hostname").on("change", function(){
|
||||
const hostname = $(this).val();
|
||||
// 检查是否包含端口号(1到5位数字)
|
||||
if (hostname.match(/:\d{1,5}$/)) {
|
||||
alert("hostname禁止带端口号");
|
||||
// 移除端口号
|
||||
$(this).val(hostname.replace(/:\d{1,5}$/,""));
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -29,7 +29,7 @@ $(function(){
|
||||
|
||||
var offset = 0;
|
||||
var duration = 0;
|
||||
|
||||
let no_warning = localStorage.getItem('no-warning');
|
||||
// 拉取现有配置
|
||||
$.get("/getsetting", function(data, status) {
|
||||
console.log(data, status);
|
||||
@@ -155,7 +155,7 @@ $(function(){
|
||||
} else {
|
||||
// 使用本地记录的
|
||||
playlist = localStorage.getItem('cur_playlist');
|
||||
if (data.includes(playlist)) {
|
||||
if (data.hasOwnProperty(playlist)) {
|
||||
$('#music_list').val(playlist);
|
||||
$('#music_list').trigger('change');
|
||||
}
|
||||
@@ -192,11 +192,34 @@ $(function(){
|
||||
});
|
||||
}
|
||||
|
||||
function do_play_music_list(listname, musicname) {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/playmusiclist",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({did: did, listname: listname, musicname: musicname}),
|
||||
success: () => {
|
||||
console.log("do_play_music_list succ", listname, musicname);
|
||||
},
|
||||
error: () => {
|
||||
console.log("do_play_music_list failed", listname, musicname);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$("#play_music_list").on("click", () => {
|
||||
var music_list = $("#music_list").val();
|
||||
var music_name = $("#music_name").val();
|
||||
let cmd = "播放列表" + music_list + "|" + music_name;
|
||||
sendcmd(cmd);
|
||||
if (no_warning) {
|
||||
do_play_music_list(music_list, music_name);
|
||||
return;
|
||||
}
|
||||
$.get(`/musicinfo?name=${music_name}`, function(data, status) {
|
||||
console.log(data);
|
||||
if (data.ret == "OK") {
|
||||
validHost(data.url) && do_play_music_list(music_list, music_name);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#web_play").on("click", () => {
|
||||
@@ -204,7 +227,7 @@ $(function(){
|
||||
$.get(`/musicinfo?name=${music_name}`, function(data, status) {
|
||||
console.log(data);
|
||||
if (data.ret == "OK") {
|
||||
$('audio').attr('src',data.url);
|
||||
validHost(data.url) && $('audio').attr('src',data.url);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -259,17 +282,32 @@ $(function(){
|
||||
$container.append($button);
|
||||
}
|
||||
|
||||
function do_play_music(musicname, searchkey) {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/playmusic",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({did: did, musicname: musicname, searchkey: searchkey}),
|
||||
success: () => {
|
||||
console.log("do_play_music succ", musicname, searchkey);
|
||||
},
|
||||
error: () => {
|
||||
console.log("do_play_music failed", musicname, searchkey);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
$("#play").on("click", () => {
|
||||
var search_key = $("#music-name").val();
|
||||
if (search_key == null) {
|
||||
search_key = "";
|
||||
}
|
||||
var filename = $("#music-filename").val();
|
||||
if (filename == null) {
|
||||
filename = "";
|
||||
if (filename == null || filename == "") {
|
||||
filename = search_key;
|
||||
}
|
||||
let cmd = "播放歌曲" + search_key + "|" + filename;
|
||||
sendcmd(cmd);
|
||||
do_play_music(filename, search_key);
|
||||
});
|
||||
|
||||
$("#volume").on('change', function () {
|
||||
@@ -375,7 +413,7 @@ $(function(){
|
||||
.catch(error => {
|
||||
console.error('Error fetching data:', error);
|
||||
});
|
||||
}, 300));
|
||||
}, 500));
|
||||
|
||||
// 动态显示保存文件名输入框
|
||||
const musicNameSelect = document.getElementById('music-name');
|
||||
@@ -386,7 +424,7 @@ $(function(){
|
||||
if (musicNameSelect.options.length === 0) {
|
||||
startsWithKeyword = false;
|
||||
} else {
|
||||
startsWithKeyword = selectedOption.text.startsWith('使用关键词联网搜索:');
|
||||
startsWithKeyword = selectedOption.text.startsWith("使用关键词播放:");
|
||||
}
|
||||
|
||||
if (startsWithKeyword) {
|
||||
@@ -435,5 +473,48 @@ $(function(){
|
||||
var minutes = Math.floor(seconds / 60);
|
||||
var remainingSeconds =Math.floor(seconds % 60);
|
||||
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
}
|
||||
|
||||
$("audio").on("error", (e) => {
|
||||
//如果audio标签的src为空,则不做任何操作,兼容安卓端的低版本webview
|
||||
if ($("audio").attr("src") === "") {
|
||||
return;
|
||||
}
|
||||
console.log('%c网页播放出现错误: ', 'color: #007acc;', e.currentTarget.error.code,e.currentTarget.error.message);
|
||||
alert(e.currentTarget.error.code==4 ? "无法打开媒体文件,XIAOMUSIC_HOSTNAME或端口地址错误,请重新设置" : "在线播放失败,请截图反馈: "+e.currentTarget.error.message);
|
||||
});
|
||||
function validHost(url) {
|
||||
//如果 localStorage 中有 no-warning 则直接返回true
|
||||
if (no_warning) {
|
||||
return true;
|
||||
}
|
||||
const local = location.host;
|
||||
const host = new URL(url).host;
|
||||
// 如果当前页面的Host与设置中的XIAOMUSIC_HOSTNAME、PORT一致, 不再提醒
|
||||
if (local === host) {
|
||||
|
||||
localStorage.setItem('no-warning', 'true');
|
||||
// 设置全局变量
|
||||
no_warning = true;
|
||||
return true;
|
||||
}
|
||||
// 如果当前页面的Host与设置中的XIAOMUSIC_HOSTNAME、PORT不一致
|
||||
const validHost = document.getElementById('valid-host');
|
||||
let validFlag = false;
|
||||
$('#local-host').text(local);
|
||||
$('#setting-host').text(host);
|
||||
validHost.showModal();
|
||||
//监听validHost的close事件
|
||||
function _handleClose() {
|
||||
console.log('%c提醒HOST不一致弹窗,用户已选择: ', 'color: #007acc;', validHost.returnValue);
|
||||
if (validHost.returnValue == "no-warning") {
|
||||
localStorage.setItem('no-warning', 'true');
|
||||
no_warning = true;
|
||||
validFlag = true;
|
||||
}
|
||||
validHost.removeEventListener('close', _handleClose)
|
||||
}
|
||||
validHost.addEventListener('close', _handleClose)
|
||||
return validFlag;
|
||||
}
|
||||
});
|
||||
74
xiaomusic/static/default_past/debug.html
Normal file
74
xiaomusic/static/default_past/debug.html
Normal file
@@ -0,0 +1,74 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>Debug For XiaoMusic</title>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="./style.css?version=1733563859">
|
||||
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
|
||||
<script src="./jquery-3.7.1.min.js?version=1733563859"></script>
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments)};
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-Z09NC1K7ZW');
|
||||
</script>
|
||||
|
||||
<!-- umami -->
|
||||
<script defer src="https://umami.hanxi.cc/script.js" data-website-id="7bfb0890-4115-4260-8892-b391513e7e99"></script>
|
||||
|
||||
<script>
|
||||
var vConsole = new window.VConsole();
|
||||
|
||||
function postJSON() {
|
||||
var data = $('#post-input').val();
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: '/debug_play_by_music_url',
|
||||
data: data,
|
||||
contentType: "application/json; charset=utf-8",
|
||||
success: (err) => {
|
||||
console.log("succ", res);
|
||||
},
|
||||
error: (res) => {
|
||||
console.log("error", res);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function sendDebugCmd() {
|
||||
var cmd = $("#cmd").val();
|
||||
var did = localStorage.getItem('cur_did');
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/cmd",
|
||||
contentType: "application/json; charset=utf-8",
|
||||
data: JSON.stringify({did: did, cmd: cmd}),
|
||||
success: () => {
|
||||
},
|
||||
error: () => {
|
||||
// 请求失败时执行的操作
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Debug For XiaoMusic</h1>
|
||||
<textarea id="post-input" rows="10" cols="50" placeholder="粘贴json数据..."></textarea><br>
|
||||
<button onclick="postJSON()">提交</button><br>
|
||||
|
||||
<hr>
|
||||
<input id="cmd" type="text"></input>
|
||||
<button onclick="sendDebugCmd()">测试自定义口令</button><br>
|
||||
</body>
|
||||
|
||||
<footer>
|
||||
<p>Powered by <a href="https://xdocs.hanxi.cc" target="_blank">XiaoMusic</a></p>
|
||||
</footer>
|
||||
</html>
|
||||
115
xiaomusic/static/default_past/downloadtool.html
Normal file
115
xiaomusic/static/default_past/downloadtool.html
Normal file
@@ -0,0 +1,115 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>歌曲下载工具</title>
|
||||
<link rel="stylesheet" type="text/css" href="./style.css?version=1733563859">
|
||||
<script src="./jquery-3.7.1.min.js?version=1733563859"></script>
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments)};
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-Z09NC1K7ZW');
|
||||
</script>
|
||||
|
||||
<!-- umami -->
|
||||
<script defer src="https://umami.hanxi.cc/script.js" data-website-id="7bfb0890-4115-4260-8892-b391513e7e99"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>歌曲下载工具</h1>
|
||||
|
||||
<div class="rows">
|
||||
<!-- 歌单的输入 -->
|
||||
<label for="playlistUrl">输入歌单 URL:</label>
|
||||
<input type="text" id="playlistUrl" value="https://m.bilibili.com/video/BV1WUsDezE88">
|
||||
|
||||
<label for="dirname">输入歌单名字:</label>
|
||||
<input type="text" id="dirname" placeholder="流行歌曲">
|
||||
|
||||
<button id="downloadPlaylistBtn">下载歌单</button>
|
||||
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="rows">
|
||||
|
||||
<!-- 单曲的输入 -->
|
||||
<label for="songUrl">输入歌曲 URL:</label>
|
||||
<input type="text" id="songUrl" value="https://m.bilibili.com/video/BV1qD4y1U7fs">
|
||||
|
||||
<label for="songName">输入歌曲名字:</label>
|
||||
<input type="text" id="songName" placeholder="歌曲名">
|
||||
|
||||
<button id="downloadSongBtn">下载单曲</button>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
// 下载歌单
|
||||
$('#downloadPlaylistBtn').click(function() {
|
||||
var playlistUrl = $('#playlistUrl').val();
|
||||
var dirname = $('#dirname').val();
|
||||
|
||||
if (!playlistUrl || !dirname) {
|
||||
alert('请填写完整的歌单 URL 和歌单名字');
|
||||
return;
|
||||
}
|
||||
|
||||
var data = {
|
||||
dirname: dirname,
|
||||
url: playlistUrl
|
||||
};
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/downloadplaylist",
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify(data),
|
||||
success: (msg) => {
|
||||
alert('歌单下载请求已发送!');
|
||||
console.log(response);
|
||||
},
|
||||
error: (msg) => {
|
||||
alert('歌单下载请求失败,请重试。');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 下载单曲
|
||||
$('#downloadSongBtn').click(function() {
|
||||
var songName = $('#songName').val();
|
||||
var songUrl = $('#songUrl').val();
|
||||
|
||||
if (!songUrl || !songName) {
|
||||
alert('请填写完整的歌曲 URL 和歌曲名字');
|
||||
return;
|
||||
}
|
||||
|
||||
var data = {
|
||||
name: songName,
|
||||
url: songUrl
|
||||
};
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/downloadonemusic",
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify(data),
|
||||
success: (msg) => {
|
||||
alert('单曲下载请求已发送!');
|
||||
console.log(response);
|
||||
},
|
||||
error: (msg) => {
|
||||
alert('单曲下载请求失败,请重试。');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
113
xiaomusic/static/default_past/index.html
Normal file
113
xiaomusic/static/default_past/index.html
Normal file
@@ -0,0 +1,113 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>小爱音箱操控面板</title>
|
||||
<script src="./jquery-3.7.1.min.js?version=1733563859"></script>
|
||||
<script src="./app.js?version=1733563859"></script>
|
||||
<link rel="stylesheet" type="text/css" href="./style.css?version=1733563859">
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments)};
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-Z09NC1K7ZW');
|
||||
</script>
|
||||
|
||||
<!-- umami -->
|
||||
<script defer src="https://umami.hanxi.cc/script.js" data-website-id="7bfb0890-4115-4260-8892-b391513e7e99"></script>
|
||||
|
||||
<!--
|
||||
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
|
||||
<script>
|
||||
var vConsole = new window.VConsole();
|
||||
</script>
|
||||
-->
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h2>小爱音箱操控面板
|
||||
(<a id="version" href="https://xdocs.hanxi.cc/issues/changelog.html">版本未知</a>)
|
||||
<span id="versionnew" class="blink"></span>
|
||||
</h2>
|
||||
<hr>
|
||||
|
||||
<div class="rows">
|
||||
<select id="did">
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="cmds">
|
||||
<a class="button" href="./setting.html">设置</a>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<div style="margin: 20px;">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="#8e43e7" style="height: 48px; width: 48px;"><path d="M550.826667 154.666667a47.786667 47.786667 0 0 0-19.84 4.48L298.666667 298.666667H186.453333A80 80 0 0 0 106.666667 378.453333v267.093334A80 80 0 0 0 186.453333 725.333333H298.666667l232.32 139.52a47.786667 47.786667 0 0 0 19.84 4.48A46.506667 46.506667 0 0 0 597.333333 822.826667V201.173333a46.506667 46.506667 0 0 0-46.506666-46.506666zM554.666667 822.826667c0 3.413333-3.84 3.84-3.84 3.84L320 688.853333l-9.6-6.186666H186.453333A37.12 37.12 0 0 1 149.333333 645.546667V378.453333A37.12 37.12 0 0 1 186.453333 341.333333h123.946667l10.24-6.186666 229.546667-137.6s3.84 0 3.84 3.84zM667.52 346.026667a21.333333 21.333333 0 0 0 0 30.293333 192 192 0 0 1 0 271.36 21.333333 21.333333 0 0 0 0 30.293333 21.333333 21.333333 0 0 0 30.293333 0 234.666667 234.666667 0 0 0 0-331.946666 21.333333 21.333333 0 0 0-30.293333 0z"></path><path d="M804.48 219.52a21.333333 21.333333 0 0 0-30.293333 30.293333 370.986667 370.986667 0 0 1 0 524.373334 21.333333 21.333333 0 0 0 0 30.293333 21.333333 21.333333 0 0 0 30.293333 0 414.08 414.08 0 0 0 0-584.96z"></path></svg>
|
||||
<input id="volume" type="range"></input>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="rows">
|
||||
<label for="search">搜索歌曲:</label>
|
||||
<input type="text" id="search" placeholder="请输入搜索关键词(如:MV高清版 周杰伦 七里香)">
|
||||
|
||||
<label for="music-name" id="music-name-label" style="display: none;">确认选择:</label>
|
||||
<select id="music-name" style="display: none;">
|
||||
<!-- 动态生成选项 -->
|
||||
</select>
|
||||
|
||||
<input id="music-filename" type="text" placeholder="请输入保存为的文件名称(如:周杰伦七里香)" style="display: none;"></input>
|
||||
<div style="display: flex; align-items: center">
|
||||
<progress id="progress" value="0" max="100" style="width: 270px"></progress>
|
||||
<div id="play-time" style="margin-left: 10px">00:00/00:00</div>
|
||||
</div>
|
||||
<div>
|
||||
<button id="play">播放</button>
|
||||
<div id="playering-music" class="text"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div class="rows">
|
||||
<label for="music_list">播放列表:</label>
|
||||
<select id="music_list"></select>
|
||||
<label for="music_name">歌曲:</label>
|
||||
<select id="music_name"></select>
|
||||
<div>
|
||||
<button id="play_music_list">播放选中歌曲</button>
|
||||
<button id="del_music">删除选中歌曲</button>
|
||||
<button id="web_play">网页播放</button>
|
||||
</div>
|
||||
<div class="play_pannel">
|
||||
<audio autoplay controls src=""></audio>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div class="rows">
|
||||
<input id="music-url" type="text" value="https://lhttp.qtfm.cn/live/4915/64k.mp3"></input>
|
||||
<button id="playurl">播放链接</button>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>Powered by <a href="https://xdocs.hanxi.cc" target="_blank">XiaoMusic</a></p>
|
||||
</footer>
|
||||
<dialog id="valid-host">
|
||||
<form method="dialog">
|
||||
<p>当前页面的HOST与设置中的HOST不一致,请检查是否设置错误</p>
|
||||
<p>当前HOST: <span id="local-host"></span></p>
|
||||
<p>设置中的HOST: <span id="setting-host"></span></p>
|
||||
<div class="btn-list">
|
||||
<a href="./setting.html" target="_blank">立即修改</a>
|
||||
<button value="no-warning" type="submit">继续并不再显示</button>
|
||||
<button value="cancle" type="submit">取消</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
</body>
|
||||
</html>
|
||||
2
xiaomusic/static/default_past/jquery-3.7.1.min.js
vendored
Normal file
2
xiaomusic/static/default_past/jquery-3.7.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
78
xiaomusic/static/default_past/m3u.html
Normal file
78
xiaomusic/static/default_past/m3u.html
Normal file
@@ -0,0 +1,78 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>M3U to JSON Converter</title>
|
||||
<link rel="stylesheet" type="text/css" href="./style.css?version=1733563859">
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments)};
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-Z09NC1K7ZW');
|
||||
</script>
|
||||
|
||||
<!-- umami -->
|
||||
<script defer src="https://umami.hanxi.cc/script.js" data-website-id="7bfb0890-4115-4260-8892-b391513e7e99"></script>
|
||||
|
||||
<!--
|
||||
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
|
||||
<script>
|
||||
// VConsole 默认会挂载到 `window.VConsole` 上
|
||||
var vConsole = new window.VConsole();
|
||||
</script>
|
||||
-->
|
||||
<script>
|
||||
function handleFileSelect(evt) {
|
||||
var file = evt.target.files[0];
|
||||
if (file) {
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
document.getElementById('m3u-input').value = e.target.result;
|
||||
};
|
||||
reader.readAsText(file);
|
||||
} else {
|
||||
alert('无法加载文件');
|
||||
}
|
||||
}
|
||||
|
||||
function convertToJSON() {
|
||||
var m3uContent = document.getElementById('m3u-input').value;
|
||||
var lines = m3uContent.split('\n');
|
||||
console.log(lines);
|
||||
var musicsArray = [];
|
||||
var currentName = '';
|
||||
lines.forEach(function(line) {
|
||||
line = line.trim();
|
||||
if (line.startsWith('#EXTINF:')) {
|
||||
currentName = line.replace(/.*,/g, '');
|
||||
} else if (line.startsWith('http') && currentName !== '') {
|
||||
musicsArray.push({"name": currentName, "type": "radio", "url": line});
|
||||
currentName = ''; // Reset the name for the next entry
|
||||
}
|
||||
});
|
||||
var output = [{
|
||||
"name": "m3u电台",
|
||||
"musics": musicsArray
|
||||
}];
|
||||
|
||||
document.getElementById('json-output').value = JSON.stringify(output, null, 2);
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>M3U to JSON Converter</h1>
|
||||
<input type="file" id="file-input" accept=".m3u" onchange="handleFileSelect(event)"/><br>
|
||||
<textarea id="m3u-input" rows="10" cols="50" placeholder="粘贴m3u内容或上传文件..."></textarea><br>
|
||||
<button onclick="convertToJSON()">转换</button><br>
|
||||
<textarea id="json-output" rows="10" cols="50" placeholder="转换后的JSON..."></textarea>
|
||||
</body>
|
||||
<footer>
|
||||
<p>Powered by <a href="https://xdocs.hanxi.cc" target="_blank">XiaoMusic</a></p>
|
||||
</footer>
|
||||
</html>
|
||||
|
||||
BIN
xiaomusic/static/default_past/qrcode.png
Normal file
BIN
xiaomusic/static/default_past/qrcode.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
270
xiaomusic/static/default_past/setting.html
Normal file
270
xiaomusic/static/default_past/setting.html
Normal file
@@ -0,0 +1,270 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<title>小爱音箱操控面板</title>
|
||||
<script src="./jquery-3.7.1.min.js?version=1733563859"></script>
|
||||
<script src="./setting.js?version=1733563859"></script>
|
||||
<link rel="stylesheet" type="text/css" href="./style.css?version=1733563859">
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments)};
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-Z09NC1K7ZW');
|
||||
</script>
|
||||
|
||||
<!-- umami -->
|
||||
<script defer src="https://umami.hanxi.cc/script.js" data-website-id="7bfb0890-4115-4260-8892-b391513e7e99"></script>
|
||||
|
||||
<!--
|
||||
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
|
||||
<script>
|
||||
var vConsole = new window.VConsole();
|
||||
</script>
|
||||
-->
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h2>小爱音箱设置面板
|
||||
(<a id="version" href="https://xdocs.hanxi.cc/issues/changelog.html">版本未知</a>)
|
||||
</h2>
|
||||
<hr>
|
||||
|
||||
<div class="rows">
|
||||
<label for="mi_did">*勾选设备(至少勾选1个):</label>
|
||||
<div id="mi_did">
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
<div id="setting">
|
||||
<div class="rows">
|
||||
<label for="account">*小米账号:</label>
|
||||
<input id="account" type="text" placeholder="填写小米登录账号" />
|
||||
|
||||
<label for="password">*小米密码:</label>
|
||||
<input id="password" type="password" placeholder="填写小米登录密码" />
|
||||
|
||||
<label for="hostname">*XIAOMUSIC_HOSTNAME(IP或域名):</label>
|
||||
<input id="hostname" type="text" />
|
||||
</div>
|
||||
<hr>
|
||||
<div class="rows">
|
||||
<label for="verbose">是否开启调试日志:</label>
|
||||
<select id="verbose">
|
||||
<option value="true" selected>true</option>
|
||||
<option value="false">false</option>
|
||||
</select>
|
||||
|
||||
<label for="port">监听端口(修改后需要重启):</label>
|
||||
<input id="port" type="number" value="8090" />
|
||||
|
||||
<label for="public_port">外网访问端口(0表示跟监听端口一致):</label>
|
||||
<input id="public_port" type="number" value="0" />
|
||||
|
||||
<label for="group_list">设备分组配置:<a href="https://github.com/hanxi/xiaomusic/issues/65#issuecomment-2215736529" target="_blank">文档</a></label>
|
||||
<input id="group_list" type="text" placeholder="did1:组名1,did2:组名1,did3:组名2" />
|
||||
|
||||
<label for="music_path">音乐目录:</label>
|
||||
<input id="music_path" type="text" value="music" />
|
||||
|
||||
<label for="download_path">音乐下载目录(必须是music的子目录):</label>
|
||||
<input id="download_path" type="text" value='music/download' />
|
||||
|
||||
<label for="conf_path">配置文件目录:</label>
|
||||
<input id="conf_path" type="text" />
|
||||
|
||||
<label for="cache_dir">缓存文件目录:</label>
|
||||
<input id="cache_dir" type="text" />
|
||||
|
||||
<label for="temp_path">临时文件目录:</label>
|
||||
<input id="temp_path" type="text" value="music/tmp" />
|
||||
|
||||
<label for="ffmpeg_location">ffmpeg路径:</label>
|
||||
<input id="ffmpeg_location" type="text" value="./ffmpeg/bin" />
|
||||
|
||||
<label for="log_file">日志路径:</label>
|
||||
<input id="log_file" type="text" value="xiaomusic.log.txt" />
|
||||
|
||||
<label for="active_cmd">允许唤醒的命令:</label>
|
||||
<input id="active_cmd" type="text" value="play,random_play,playlocal,play_music_list,stop" />
|
||||
|
||||
<label for="exclude_dirs">忽略目录(逗号分割):</label>
|
||||
<input id="exclude_dirs" type="text" value="@eaDir,tmp" />
|
||||
|
||||
<label for="ignore_tag_dirs">不扫描标签信息目录(逗号分割):</label>
|
||||
<input id="ignore_tag_dirs" type="text" value="" />
|
||||
|
||||
<label for="music_path_depth">目录深度:</label>
|
||||
<input id="music_path_depth" type="number" value="10" />
|
||||
|
||||
<label for="search_prefix">XIAOMUSIC_SEARCH(歌曲下载方式):</label>
|
||||
<select id="search_prefix">
|
||||
<option value="bilisearch:">bilisearch:</option>
|
||||
<option value="ytsearch:">ytsearch:</option>
|
||||
</select>
|
||||
|
||||
<label for="proxy">XIAOMUSIC_PROXY(ytsearch需要):</label>
|
||||
<input id="proxy" type="text" placeholder="http://192.168.2.5:8080" />
|
||||
|
||||
<label for="remove_id3tag">去除MP3 ID3v2和填充:</label>
|
||||
<select id="remove_id3tag">
|
||||
<option value="true">true</option>
|
||||
<option value="false" selected>false</option>
|
||||
</select>
|
||||
|
||||
<label for="convert_to_mp3">转换为MP3:</label>
|
||||
<select id="convert_to_mp3">
|
||||
<option value="true">true</option>
|
||||
<option value="false" selected>false</option>
|
||||
</select>
|
||||
|
||||
<label for="miio_tts_command">MiIO tts 指令(解决部分型号没有提示音的问题):</label>
|
||||
<input id="miio_tts_command" type="text" placeholder="如:5 或者 5-3" />
|
||||
|
||||
<label for="disable_httpauth">关闭控制台密码验证:</label>
|
||||
<select id="disable_httpauth">
|
||||
<option value="true" selected>true</option>
|
||||
<option value="false">false</option>
|
||||
</select>
|
||||
<label for="httpauth_username">控制台账户:</label>
|
||||
<input id="httpauth_username" type="text" value="" />
|
||||
<label for="httpauth_password">控制台密码:</label>
|
||||
<input id="httpauth_password" type="password" value="" />
|
||||
|
||||
<label for="disable_download">关闭下载功能:</label>
|
||||
<select id="disable_download">
|
||||
<option value="true">true</option>
|
||||
<option value="false" selected>false</option>
|
||||
</select>
|
||||
|
||||
<label for="use_music_audio_id">触屏版显示歌曲ID:</label>
|
||||
<input id="use_music_audio_id" type="text" value="1582971365183456177" />
|
||||
<label for="use_music_id">触屏版显示歌曲分段ID:</label>
|
||||
<input id="use_music_id" type="text" value="355454500" />
|
||||
|
||||
<label for="fuzzy_match_cutoff">模糊匹配阈值(0.1~0.9):</label>
|
||||
<input id="fuzzy_match_cutoff" type="number" value="0.6" />
|
||||
|
||||
<label for="enable_fuzzy_match">开启模糊搜索:</label>
|
||||
<select id="enable_fuzzy_match">
|
||||
<option value="true" selected>true</option>
|
||||
<option value="false">false</option>
|
||||
</select>
|
||||
|
||||
<label for="use_music_api">型号兼容模式:</label>
|
||||
<select id="use_music_api">
|
||||
<option value="true">true</option>
|
||||
<option value="false" selected>false</option>
|
||||
</select>
|
||||
|
||||
<label for="continue_play">启用继续播放(可能导致兼容性问题):</label>
|
||||
<select id="continue_play">
|
||||
<option value="true">true</option>
|
||||
<option value="false" selected>false</option>
|
||||
</select>
|
||||
|
||||
<label for="pull_ask_sec">获取对话记录间隔(秒):</label>
|
||||
<input id="pull_ask_sec" type="number" value="1" />
|
||||
|
||||
<label for="delay_sec">下一首歌延迟播放秒数:</label>
|
||||
<input id="delay_sec" type="number" value="3" />
|
||||
|
||||
<label for="stop_tts_msg">停止提示音:</label>
|
||||
<input id="stop_tts_msg" type="text" value="收到,再见" />
|
||||
<label for="play_type_one_tts_msg">单曲循环提示音:</label>
|
||||
<input id="play_type_one_tts_msg" type="text" value="已经设置为单曲循环" />
|
||||
<label for="play_type_all_tts_msg">全部循环提示音:</label>
|
||||
<input id="play_type_all_tts_msg" type="text" value="已经设置为全部循环" />
|
||||
<label for="play_type_rnd_tts_msg">随机播放提示音:</label>
|
||||
<input id="play_type_rnd_tts_msg" type="text" value="已经设置为随机播放" />
|
||||
<label for="play_type_sin_tts_msg">单曲播放提示音:</label>
|
||||
<input id="play_type_sin_tts_msg" type="text" value="已经设置为单曲播放" />
|
||||
<label for="play_type_seq_tts_msg">顺序播放提示音:</label>
|
||||
<input id="play_type_seq_tts_msg" type="text" value="已经设置为顺序播放" />
|
||||
|
||||
<label for="keywords_playlocal">播放本地歌曲口令:</label>
|
||||
<input id="keywords_playlocal" type="text" value="播放本地歌曲,本地播放歌曲" />
|
||||
<label for="keywords_play">播放歌曲口令:</label>
|
||||
<input id="keywords_play" type="text" value="播放歌曲,放歌曲" />
|
||||
<label for="keywords_playlist">播放列表口令:</label>
|
||||
<input id="keywords_playlist" type="text" value="播放列表,播放歌单" />
|
||||
<label for="keywords_stop">停止口令:</label>
|
||||
<input id="keywords_stop" type="text" value="关机,暂停,停止,停止播放" />
|
||||
<label for="keywords_search_playlocal">本地搜索播放口令(会产生临时播放列表):</label>
|
||||
<input id="keywords_search_playlocal" type="text" value="本地搜索播放" />
|
||||
<label for="keywords_search_play">搜索播放口令(会产生临时播放列表):</label>
|
||||
<input id="keywords_search_play" type="text" value="搜索播放" />
|
||||
|
||||
<label for="enable_yt_dlp_cookies">启用yt-dlp-cookies(需要先上传yt-dlp-cookies.txt文件):</label>
|
||||
<select id="enable_yt_dlp_cookies">
|
||||
<option value="true">true</option>
|
||||
<option value="false" selected>false</option>
|
||||
</select>
|
||||
|
||||
<label for="enable_save_tag">启用ID3标签写入文件:</label>
|
||||
<select id="enable_save_tag">
|
||||
<option value="true">true</option>
|
||||
<option value="false" selected>false</option>
|
||||
</select>
|
||||
|
||||
<label for="get_ask_by_mina">特殊型号获取对话记录:</label>
|
||||
<select id="get_ask_by_mina">
|
||||
<option value="true">true</option>
|
||||
<option value="false" selected>false</option>
|
||||
</select>
|
||||
|
||||
<label for="recently_added_playlist_len">最近新增的歌曲数量:</label>
|
||||
<input id="recently_added_playlist_len" type="number" value="50" />
|
||||
|
||||
<label for="music_list_url">歌单地址:</label>
|
||||
<input id="music_list_url" type="text" value="https://gist.githubusercontent.com/hanxi/dda82d964a28f8110f8fba81c3ff8314/raw/example.json" />
|
||||
|
||||
<label for="music_list_json">歌单内容:<a href="https://github.com/hanxi/xiaomusic/issues/78" target="_blank">格式文档</a></label>
|
||||
<textarea id="music_list_json" type="text"></textarea>
|
||||
|
||||
<label for="crontab_json">定时任务:<a href="https://github.com/hanxi/xiaomusic/issues/182" target="_blank">格式文档</a></label>
|
||||
<textarea id="crontab_json" type="text"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<div class="rows">
|
||||
<label for="yt_dlp_cookies_file">上传yt_dlp_cookies.txt文件:<a href="https://github.com/hanxi/xiaomusic/issues/210" target="_blank">文档</a></label>
|
||||
<input id="yt_dlp_cookies_file" name="file" type="file">
|
||||
<button id="upload_yt_dlp_cookie">上传</button>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<button onclick="location.href='/static/default/index.html';">返回首页</button>
|
||||
<button id="get_music_list">获取歌单</button>
|
||||
<button class="save-button">保存</button>
|
||||
<hr>
|
||||
|
||||
<button id="refresh_music_tag">刷新tag</button>
|
||||
<button id="clear_cache">清空缓存</button>
|
||||
<a class="button" href="/downloadlog" download="xiaomusic.txt">下载日志文件</a>
|
||||
|
||||
<hr>
|
||||
|
||||
<button onclick="location.href='/docs';">查看接口文档</button>
|
||||
<a class="button" href="./m3u.html" target="_blank">m3u文件转换</a>
|
||||
<a class="button" href="./downloadtool.html" target="_blank">歌曲下载工具</a>
|
||||
<hr>
|
||||
|
||||
<a class="button" href="./debug.html" target="_blank">调试工具</a>
|
||||
<a class="button" href="https://afdian.com/a/imhanxi" target="_blank">💰 爱发电</a>
|
||||
<a class="button" href="https://github.com/hanxi/xiaomusic" target="_blank">点个 Star ⭐</a>
|
||||
|
||||
<div class="rows">
|
||||
<img class="qrcode" src="./qrcode.png" alt="请涵曦喝奶茶🧋">
|
||||
</div>
|
||||
<footer>
|
||||
<p>Powered by <a href="https://xdocs.hanxi.cc" target="_blank">XiaoMusic</a></p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
203
xiaomusic/static/default_past/setting.js
Normal file
203
xiaomusic/static/default_past/setting.js
Normal file
@@ -0,0 +1,203 @@
|
||||
$(function(){
|
||||
// 拉取版本
|
||||
$.get("/getversion", function(data, status) {
|
||||
console.log(data, status, data["version"]);
|
||||
$("#version").text(`${data.version}`);
|
||||
});
|
||||
|
||||
// 遍历所有的select元素,默认选中只有1个选项的
|
||||
const autoSelectOne = () => {
|
||||
$('select').each(function() {
|
||||
// 如果select元素仅有一个option子元素
|
||||
if ($(this).children('option').length === 1) {
|
||||
// 选中这个option
|
||||
$(this).find('option').prop('selected', true);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function updateCheckbox(selector, mi_did, device_list,accountPassValid) {
|
||||
// 清除现有的内容
|
||||
$(selector).empty();
|
||||
|
||||
// 将 mi_did 字符串通过逗号分割转换为数组,以便于判断默认选中项
|
||||
var selected_dids = mi_did.split(',');
|
||||
|
||||
//如果device_list为空,则可能是未设置小米账号密码或者已设置密码,但是没有过小米验证,此处需要提示用户
|
||||
if (device_list.length == 0) {
|
||||
const loginTips = accountPassValid ? `<div class="login-tips">未发现可用的小爱设备,请检查账号密码是否输错,并关闭加速代理或在<a href="https://www.mi.com">小米官网</a>登陆过人脸或滑块验证。如仍未解决。请根据<a href="https://github.com/hanxi/xiaomusic/issues/99">FAQ</a>的内容解决问题。</div>` : `<div class="login-tips">未发现可用的小爱设备,请先在下面的输入框中设置小米的<b>账号、密码</b></div>`;
|
||||
$(selector).append(loginTips);
|
||||
return;
|
||||
}
|
||||
$.each(device_list, function(index, device) {
|
||||
var did = device.miotDID;
|
||||
var hardware = device.hardware;
|
||||
var name = device.name;
|
||||
// 创建复选框元素
|
||||
var checkbox = $('<input>', {
|
||||
type: 'checkbox',
|
||||
id: did,
|
||||
value: `${did}`,
|
||||
class: 'custom-checkbox', // 添加样式类
|
||||
// 如果mi_did中包含了该did,则默认选中
|
||||
checked: selected_dids.indexOf(did) !== -1
|
||||
});
|
||||
|
||||
// 创建标签元素
|
||||
var label = $('<label>', {
|
||||
for: did,
|
||||
class: 'checkbox-label', // 添加样式类
|
||||
text: `【${hardware} ${did}】${name}` // 设定标签内容
|
||||
});
|
||||
|
||||
// 将复选框和标签添加到目标选择器元素中
|
||||
$(selector).append(checkbox).append(label);
|
||||
});
|
||||
}
|
||||
|
||||
function getSelectedDids(containerSelector) {
|
||||
var selectedDids = [];
|
||||
|
||||
// 仅选择给定容器中选中的复选框
|
||||
$(containerSelector + ' .custom-checkbox:checked').each(function() {
|
||||
var did = this.value;
|
||||
selectedDids.push(did);
|
||||
});
|
||||
|
||||
return selectedDids.join(',');
|
||||
}
|
||||
|
||||
// 拉取现有配置
|
||||
$.get("/getsetting?need_device_list=true", function(data, status) {
|
||||
console.log(data, status);
|
||||
const accountPassValid = data.account && data.password;
|
||||
updateCheckbox("#mi_did", data.mi_did, data.device_list, accountPassValid);
|
||||
|
||||
// 初始化显示
|
||||
for (const key in data) {
|
||||
const $element = $("#" + key);
|
||||
if ($element.length) {
|
||||
if (data[key] === true) {
|
||||
$element.val('true');
|
||||
} else if (data[key] === false) {
|
||||
$element.val('false');
|
||||
} else {
|
||||
$element.val(data[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
autoSelectOne();
|
||||
});
|
||||
|
||||
$(".save-button").on("click", () => {
|
||||
var setting = $('#setting');
|
||||
var inputs = setting.find('input, select, textarea');
|
||||
var data = {};
|
||||
inputs.each(function() {
|
||||
var id = this.id;
|
||||
if (id) {
|
||||
data[id] = $(this).val();
|
||||
}
|
||||
});
|
||||
var did_list = getSelectedDids("#mi_did");
|
||||
data["mi_did"] = did_list;
|
||||
console.log(data)
|
||||
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/savesetting",
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify(data),
|
||||
success: (msg) => {
|
||||
alert(msg);
|
||||
location.reload();
|
||||
},
|
||||
error: (msg) => {
|
||||
alert(msg);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#get_music_list").on("click", () => {
|
||||
var music_list_url = $("#music_list_url").val();
|
||||
console.log("music_list_url", music_list_url);
|
||||
var data = {
|
||||
url: music_list_url,
|
||||
};
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/downloadjson",
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify(data),
|
||||
success: (res) => {
|
||||
if (res.ret == "OK") {
|
||||
$("#music_list_json").val(res.content);
|
||||
} else {
|
||||
console.log(res);
|
||||
alert(res.ret);
|
||||
}
|
||||
},
|
||||
error: (res) => {
|
||||
console.log(res);
|
||||
alert(res);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#refresh_music_tag").on("click", () => {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: "/refreshmusictag",
|
||||
contentType: "application/json",
|
||||
success: (res) => {
|
||||
console.log(res);
|
||||
alert(res.ret);
|
||||
},
|
||||
error: (res) => {
|
||||
console.log(res);
|
||||
alert(res);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#upload_yt_dlp_cookie").on("click", () => {
|
||||
var fileInput = document.getElementById('yt_dlp_cookies_file');
|
||||
var file = fileInput.files[0]; // 获取文件对象
|
||||
if (file) {
|
||||
var formData = new FormData();
|
||||
formData.append("file", file);
|
||||
$.ajax({
|
||||
url: "/uploadytdlpcookie",
|
||||
type: "POST",
|
||||
data: formData,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
success: function(res) {
|
||||
console.log(res);
|
||||
alert("上传成功");
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
console.log(res);
|
||||
alert("上传失败");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
alert("请选择一个文件");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
$("#clear_cache").on("click", () => {
|
||||
localStorage.clear();
|
||||
});
|
||||
$("#hostname").on("change", function(){
|
||||
const hostname = $(this).val();
|
||||
// 检查是否包含端口号(1到5位数字)
|
||||
if (hostname.match(/:\d{1,5}$/)) {
|
||||
alert("hostname禁止带端口号");
|
||||
// 移除端口号
|
||||
$(this).val(hostname.replace(/:\d{1,5}$/,""));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -21,7 +21,6 @@ button:active, .button:active {
|
||||
}
|
||||
label {
|
||||
margin-left: 10px;
|
||||
width: 300px;
|
||||
}
|
||||
input,select {
|
||||
margin-left: 5%;
|
||||
@@ -31,6 +30,7 @@ input,select {
|
||||
width: 90%;
|
||||
max-width: 400px;
|
||||
height: 40px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.rows {
|
||||
@@ -100,3 +100,67 @@ footer {
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
|
||||
.login-tips {
|
||||
color: red;
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.login-tips a {
|
||||
color: rgb(9, 105, 218);
|
||||
text-decoration: underline;
|
||||
}
|
||||
#valid-host {
|
||||
padding: 20px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
#valid-host::backdrop {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
|
||||
#valid-host form input {
|
||||
width: fit-content;
|
||||
margin: 0;
|
||||
height: fit-content;
|
||||
}
|
||||
#valid-host p {
|
||||
word-break: break-all;
|
||||
}
|
||||
#valid-host p span {
|
||||
color: red;
|
||||
}
|
||||
#valid-host a, #valid-host a:visited {
|
||||
color: rgb(9, 105, 218);;
|
||||
text-decoration: underline;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
#valid-host a:hover {
|
||||
color: rgb(9, 95, 198);
|
||||
}
|
||||
#valid-host .btn-list {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
#valid-host .btn-list button {
|
||||
width: fit-content;
|
||||
min-width: 60px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
color: white;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
border-radius: 10px;
|
||||
background-color: #008CBA;
|
||||
}
|
||||
#valid-host .btn-list button:hover {
|
||||
font-weight:bold;
|
||||
background-color: #007CBA;
|
||||
}
|
||||
@@ -2,36 +2,41 @@
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="manifest" href="/static/manifest.json">
|
||||
<link rel="icon" href="/static/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="manifest" href="/static/manifest.json" />
|
||||
<link rel="icon" href="/static/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>小爱音箱操控面板</title>
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments)};
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-Z09NC1K7ZW');
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag() {
|
||||
dataLayer.push(arguments);
|
||||
}
|
||||
gtag("js", new Date());
|
||||
gtag("config", "G-Z09NC1K7ZW");
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
if (navigator.serviceWorker != null) {
|
||||
navigator.serviceWorker.register('/static/sw.js')
|
||||
.then(function(registration) {
|
||||
console.log('Registered events at scope: ', registration.scope);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<!-- umami -->
|
||||
<script defer src="https://umami.hanxi.cc/script.js" data-website-id="7bfb0890-4115-4260-8892-b391513e7e99"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
if (navigator.serviceWorker != null) {
|
||||
navigator.serviceWorker
|
||||
.register("/static/sw.js")
|
||||
.then(function (registration) {
|
||||
console.log("Registered events at scope: ", registration.scope);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container_wrapper">
|
||||
<div class="logo">
|
||||
<img src="/static/xiaoai.png" alt="">
|
||||
<img src="/static/xiaoai.png" alt="" />
|
||||
</div>
|
||||
<div class="desc">
|
||||
<h1>谁家灯火夜通明</h1>
|
||||
@@ -40,32 +45,229 @@
|
||||
</div>
|
||||
<div class="options">
|
||||
<!-- 选择主题 /static/[theme] -->
|
||||
<a href="/static/default/index.html" class="href">默认主题</a>
|
||||
<a href="/static/pure/index.html" class="href">Pure主题</a>
|
||||
<a href="/static/xplayer/index.html" class="href">XMusicPlayer</a>
|
||||
<a href="https://afdian.com/a/imhanxi" target="_blank">爱发电</a>
|
||||
<a href="https://github.com/hanxi/xiaomusic" target="_blank">GitHub</a>
|
||||
<div class="options_list">
|
||||
<a href="/static/default/index.html">默认主题</a>
|
||||
</div>
|
||||
<div class="options_list">
|
||||
<a href="/static/default_past/index.html">怀旧主题</a>
|
||||
</div>
|
||||
<div class="options_list">
|
||||
<a href="/static/pure/index.html">Pure主题</a>
|
||||
</div>
|
||||
<div class="options_list">
|
||||
<a href="/static/xplayer/index.html">XMusicPlayer</a>
|
||||
</div>
|
||||
<div class="options_list weapp">
|
||||
<a href="https://github.com/F-loat/xiaoplayer" target="_blank">微信小程序</a>
|
||||
<iframe width="240px" height="240px" src="/static/weapp/qrcode.html"></iframe>
|
||||
</div>
|
||||
<div class="options_list">
|
||||
<a href="https://afdian.com/a/imhanxi" target="_blank">爱发电</a>
|
||||
</div>
|
||||
<div class="options_list">
|
||||
<a href="https://github.com/hanxi/xiaomusic" target="_blank">GitHub</a>
|
||||
</div>
|
||||
<div class="options_list">
|
||||
<a href="https://github.com/hanxi/xiaomusic/issues/211" target="_blank">帮助</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
power by <a href="https://github.com/hanxi/xiaomusic">XiaoMusic</a>
|
||||
Powered by <a href="https://xdocs.hanxi.cc">XiaoMusic</a>
|
||||
</footer>
|
||||
<style>
|
||||
@font-face{ font-family: "得意黑 斜体"; font-weight: 400; src: url("//at.alicdn.com/wf/webfont/603VmyqiyGMz/gJk2ny0v51vn.woff2") format("woff2"), url("//at.alicdn.com/wf/webfont/603VmyqiyGMz/e2C1wSBHH86h.woff") format("woff"); font-display: swap;} @font-face{ font-family: "阿里妈妈数黑体 Bold"; font-weight: 700; src: url("//at.alicdn.com/wf/webfont/603VmyqiyGMz/4DWYdFK3dz5J.woff2") format("woff2"), url("//at.alicdn.com/wf/webfont/603VmyqiyGMz/V7EBEKlNSdxC.woff") format("woff"); font-display: swap;} body{ background-color: rgb(47, 44, 67); height: 100%; overflow: hidden;}
|
||||
.container_wrapper{display: flex; justify-content: space-around; align-items: center; flex-wrap: wrap; height: 90vh; cursor: default;}
|
||||
h1{ font-weight: bold; color: #a2a9af; max-width: 600px; font-family: '得意黑 斜体', 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif; font-size: 2.5em; border-bottom: 1px solid #a2a9af;}
|
||||
.container_wrapper .logo img{ width: 140px; height: auto; filter: drop-shadow(10px 10px 10px rgba(0, 0, 0, 0.5));}
|
||||
.desc{ text-align: center; color: #fff; margin: auto 30px; backdrop-filter: blur(5px);}
|
||||
.desc p{ font-size: 1.2em; margin: 0; padding: 0; font-family: '阿里妈妈数黑体 Bold'; font-weight: 800;}
|
||||
p.call{ letter-spacing: 0.4em; font-size: 2.2em; line-height: 1.5; font-style: normal;}
|
||||
p.answer{ letter-spacing: 0.23em; line-height: 1.5; font-style: normal; color: #a2a9af; margin-top: 10px;}
|
||||
.desc p::before, .desc p::after{ font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif; font-size: 1.5em; color: #4c5870;}
|
||||
.desc p::before{ content: "“";} .desc p::after{ content: "”";}
|
||||
.options{ display: flex; flex-direction: column;}
|
||||
.options a{ color: #a2a9af; text-decoration: none; font-size: 1.1em; position: relative; display: inline; margin: 10px auto;}
|
||||
.options a::before{ content: ''; position: absolute; bottom: 0; left: 0; right: 0; height: 2px; background-color: #ebedec; transform-origin: bottom right; transform: scaleX(0); transition: transform 0.3s ease;}
|
||||
.options a:hover::before{ transform-origin: bottom left; transform: scaleX(1);} .options a:hover{ color:#ebedec;}
|
||||
footer{ display: flex; justify-content: center; color: #4c5870;} footer a{ color:inherit; text-decoration: none; margin: auto 10px;}
|
||||
@font-face {
|
||||
font-family: "得意黑 斜体";
|
||||
font-weight: 400;
|
||||
src: url("//at.alicdn.com/wf/webfont/603VmyqiyGMz/gJk2ny0v51vn.woff2") format("woff2"),
|
||||
url("//at.alicdn.com/wf/webfont/603VmyqiyGMz/e2C1wSBHH86h.woff") format("woff");
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "阿里妈妈数黑体 Bold";
|
||||
font-weight: 700;
|
||||
src: url("//at.alicdn.com/wf/webfont/603VmyqiyGMz/4DWYdFK3dz5J.woff2") format("woff2"),
|
||||
url("//at.alicdn.com/wf/webfont/603VmyqiyGMz/V7EBEKlNSdxC.woff") format("woff");
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: rgb(47, 44, 67);
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container_wrapper {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
height: 90vh;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: bold;
|
||||
color: #a2a9af;
|
||||
max-width: 600px;
|
||||
font-family: "得意黑 斜体", "Lucida Sans", "Lucida Sans Regular",
|
||||
"Lucida Grande", "Lucida Sans Unicode", Geneva, Verdana, sans-serif;
|
||||
font-size: 2.5em;
|
||||
border-bottom: 1px solid #a2a9af;
|
||||
}
|
||||
|
||||
.container_wrapper .logo img {
|
||||
width: 140px;
|
||||
height: auto;
|
||||
filter: drop-shadow(10px 10px 10px rgba(0, 0, 0, 0.5));
|
||||
}
|
||||
|
||||
.desc {
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
margin: auto 30px;
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.desc p {
|
||||
font-size: 1.2em;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: "阿里妈妈数黑体 Bold";
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
p.call {
|
||||
letter-spacing: 0.4em;
|
||||
font-size: 2.2em;
|
||||
line-height: 1.5;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
p.answer {
|
||||
letter-spacing: 0.23em;
|
||||
line-height: 1.5;
|
||||
font-style: normal;
|
||||
color: #a2a9af;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.desc p::before,
|
||||
.desc p::after {
|
||||
font-family: "Lucida Sans", "Lucida Sans Regular", "Lucida Grande",
|
||||
"Lucida Sans Unicode", Geneva, Verdana, sans-serif;
|
||||
font-size: 1.5em;
|
||||
color: #4c5870;
|
||||
}
|
||||
|
||||
.desc p::before {
|
||||
content: "“";
|
||||
}
|
||||
|
||||
.desc p::after {
|
||||
content: "”";
|
||||
}
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.options .options_list {
|
||||
font-size: 1.1em;
|
||||
position: relative;
|
||||
margin: 5px 2px;
|
||||
border: 1px solid #a2a9af;
|
||||
padding: 10px 20px;
|
||||
border-radius: 10px;
|
||||
color: #a2a9af;
|
||||
transition: all 0.3s ease-in-out;
|
||||
width: 120px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.options a {
|
||||
color: #a2a9af;
|
||||
text-decoration: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.options a::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background-color: #ebedec;
|
||||
transform-origin: bottom right;
|
||||
transform: scaleX(0);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.options a:hover::before {
|
||||
transform-origin: bottom left;
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
.options a:hover {
|
||||
color: #ebedec;
|
||||
}
|
||||
|
||||
.weapp:hover iframe {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.weapp iframe {
|
||||
display: none;
|
||||
border: none;
|
||||
position: absolute;
|
||||
top: -80px;
|
||||
right: 180px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 510px) {
|
||||
.weapp iframe {
|
||||
position: fixed;
|
||||
top: 40%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
.options{
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.options .options_list {
|
||||
width: 100px;
|
||||
}
|
||||
h1{
|
||||
margin: 0;
|
||||
}
|
||||
p.call {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
color: #4c5870;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
margin: auto 10px;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-Z09NC1K7ZW');
|
||||
</script>
|
||||
|
||||
<!-- umami -->
|
||||
<script defer src="https://umami.hanxi.cc/script.js" data-website-id="7bfb0890-4115-4260-8892-b391513e7e99"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
60
xiaomusic/static/weapp/qrcode.html
Normal file
60
xiaomusic/static/weapp/qrcode.html
Normal file
@@ -0,0 +1,60 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Weapp QRCode</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#qrcode {
|
||||
flex: 1;
|
||||
max-width: 180px;
|
||||
border-radius: 12px;
|
||||
background-color: white;
|
||||
padding: 8px;
|
||||
}
|
||||
</style>
|
||||
<script src="https://web-9gikcbug35bad3a8-1304825656.tcloudbaseapp.com/sdk/1.4.0/cloud.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<img id="qrcode" src="https://assets-1251785959.cos.ap-beijing.myqcloud.com/xiaoplayer/weappcode.jpg" />
|
||||
<script>
|
||||
const c1 = new cloud.Cloud({
|
||||
identityless: true,
|
||||
resourceAppid: 'wx5931d820da6e8e50',
|
||||
resourceEnv: 'cards-ahoy-3g50hglqe5f630e4'
|
||||
})
|
||||
|
||||
c1.init().then(() => {
|
||||
c1.callFunction({
|
||||
name: 'qrcode',
|
||||
data: {
|
||||
host: location.host,
|
||||
protocol: location.protocol
|
||||
},
|
||||
complete: (res) => {
|
||||
if (res.errMsg === 'cloud.callFunction:ok' && !res.result.errCode) {
|
||||
const blob = new Blob([res.result.buffer])
|
||||
const url = URL.createObjectURL(blob)
|
||||
document.querySelector('#qrcode').setAttribute('src', url)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -7,6 +7,20 @@
|
||||
<title>XMusicPlayer</title>
|
||||
<script type="module" crossorigin src="/static/xplayer/assets/index-C1eAAj9j.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/static/xplayer/assets/index-BBmHnUeL.css">
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z09NC1K7ZW"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag() {
|
||||
dataLayer.push(arguments);
|
||||
}
|
||||
gtag("js", new Date());
|
||||
gtag("config", "G-Z09NC1K7ZW");
|
||||
</script>
|
||||
|
||||
<!-- umami -->
|
||||
<script defer src="https://umami.hanxi.cc/script.js" data-website-id="7bfb0890-4115-4260-8892-b391513e7e99"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import json
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
import platform
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
@@ -27,7 +28,19 @@ import aiohttp
|
||||
import mutagen
|
||||
from mutagen.asf import ASF
|
||||
from mutagen.flac import FLAC
|
||||
from mutagen.id3 import APIC, ID3, Encoding, TextFrame, TimeStampTextFrame
|
||||
from mutagen.id3 import (
|
||||
APIC,
|
||||
ID3,
|
||||
TALB,
|
||||
TCON,
|
||||
TDRC,
|
||||
TIT2,
|
||||
TPE1,
|
||||
USLT,
|
||||
Encoding,
|
||||
TextFrame,
|
||||
TimeStampTextFrame,
|
||||
)
|
||||
from mutagen.mp3 import MP3
|
||||
from mutagen.mp4 import MP4
|
||||
from mutagen.oggvorbis import OggVorbis
|
||||
@@ -323,7 +336,7 @@ async def get_local_music_duration(filename, ffmpeg_location="./ffmpeg/bin"):
|
||||
m = await loop.run_in_executor(None, mutagen.File, filename)
|
||||
duration = m.info.length
|
||||
except Exception as e:
|
||||
log.error(f"Error getting local music {filename} duration: {e}")
|
||||
log.warning(f"Error getting local music {filename} duration: {e}")
|
||||
return duration
|
||||
|
||||
|
||||
@@ -568,6 +581,16 @@ class Metadata:
|
||||
picture: str = ""
|
||||
lyrics: str = ""
|
||||
|
||||
def __init__(self, info=None):
|
||||
if info:
|
||||
self.title = info.get("title", "")
|
||||
self.artist = info.get("artist", "")
|
||||
self.album = info.get("album", "")
|
||||
self.year = info.get("year", "")
|
||||
self.genre = info.get("genre", "")
|
||||
self.picture = info.get("picture", "")
|
||||
self.lyrics = info.get("lyrics", "")
|
||||
|
||||
|
||||
def _get_alltag_value(tags, k):
|
||||
v = tags.getall(k)
|
||||
@@ -596,6 +619,15 @@ def _to_utf8(v):
|
||||
return str(v)
|
||||
|
||||
|
||||
def save_picture_by_base64(picture_base64_data, save_root, file_path):
|
||||
try:
|
||||
picture_data = base64.b64decode(picture_base64_data)
|
||||
except (TypeError, ValueError) as e:
|
||||
log.exception(f"Error decoding base64 data: {e}")
|
||||
return None
|
||||
return _save_picture(picture_data, save_root, file_path)
|
||||
|
||||
|
||||
def _save_picture(picture_data, save_root, file_path):
|
||||
# 计算文件名的哈希值
|
||||
file_hash = hashlib.md5(file_path.encode("utf-8")).hexdigest()
|
||||
@@ -611,14 +643,19 @@ def _save_picture(picture_data, save_root, file_path):
|
||||
try:
|
||||
_resize_save_image(picture_data, picture_path)
|
||||
except Exception as e:
|
||||
log.exception(f"Error _resize_save_image: {e}")
|
||||
log.warning(f"Error _resize_save_image: {e}")
|
||||
return picture_path
|
||||
|
||||
|
||||
def _resize_save_image(image_bytes, save_path, max_size=300):
|
||||
# 将 bytes 转换为 PIL Image 对象
|
||||
image = Image.open(io.BytesIO(image_bytes))
|
||||
image = image.convert("RGB")
|
||||
image = None
|
||||
try:
|
||||
image = Image.open(io.BytesIO(image_bytes))
|
||||
image = image.convert("RGB")
|
||||
except Exception as e:
|
||||
log.warning(f"Error _resize_save_image: {e}")
|
||||
return
|
||||
|
||||
# 获取原始尺寸
|
||||
original_width, original_height = image.size
|
||||
@@ -641,8 +678,16 @@ def _resize_save_image(image_bytes, save_path, max_size=300):
|
||||
|
||||
|
||||
def extract_audio_metadata(file_path, save_root):
|
||||
audio = mutagen.File(file_path)
|
||||
metadata = Metadata()
|
||||
|
||||
audio = None
|
||||
try:
|
||||
audio = mutagen.File(file_path)
|
||||
except Exception as e:
|
||||
log.warning(f"Error extract_audio_metadata file: {file_path} {e}")
|
||||
if audio is None:
|
||||
return asdict(metadata)
|
||||
|
||||
tags = audio.tags
|
||||
if tags is None:
|
||||
return asdict(metadata)
|
||||
@@ -722,6 +767,110 @@ def extract_audio_metadata(file_path, save_root):
|
||||
return asdict(metadata)
|
||||
|
||||
|
||||
def set_music_tag_to_file(file_path, info):
|
||||
audio = mutagen.File(file_path, easy=True)
|
||||
if audio is None:
|
||||
log.error(f"Unable to open file {file_path}")
|
||||
return "Unable to open file"
|
||||
|
||||
if isinstance(audio, MP3):
|
||||
_set_mp3_tags(audio, info)
|
||||
elif isinstance(audio, FLAC):
|
||||
_set_flac_tags(audio, info)
|
||||
elif isinstance(audio, MP4):
|
||||
_set_mp4_tags(audio, info)
|
||||
elif isinstance(audio, OggVorbis):
|
||||
_set_ogg_tags(audio, info)
|
||||
elif isinstance(audio, ASF):
|
||||
_set_asf_tags(audio, info)
|
||||
elif isinstance(audio, WAVE):
|
||||
_set_wave_tags(audio, info)
|
||||
else:
|
||||
log.error(f"Unsupported file type for {file_path}")
|
||||
return "Unsupported file type"
|
||||
|
||||
try:
|
||||
audio.save()
|
||||
log.info(f"Tags saved successfully to {file_path}")
|
||||
return "OK"
|
||||
except Exception as e:
|
||||
log.exception(f"Error saving tags: {e}")
|
||||
return "Error saving tags"
|
||||
|
||||
|
||||
def _set_mp3_tags(audio, info):
|
||||
audio["TIT2"] = TIT2(encoding=3, text=info.title)
|
||||
audio["TPE1"] = TPE1(encoding=3, text=info.artist)
|
||||
audio["TALB"] = TALB(encoding=3, text=info.album)
|
||||
audio["TDRC"] = TDRC(encoding=3, text=info.year)
|
||||
audio["TCON"] = TCON(encoding=3, text=info.genre)
|
||||
if info.lyrics:
|
||||
audio["USLT"] = USLT(encoding=3, lang="eng", text=info.lyrics)
|
||||
if info.picture:
|
||||
with open(info.picture, "rb") as img_file:
|
||||
image_data = img_file.read()
|
||||
audio["APIC"] = APIC(
|
||||
encoding=3, mime="image/jpeg", type=3, desc="Cover", data=image_data
|
||||
)
|
||||
|
||||
|
||||
def _set_flac_tags(audio, info):
|
||||
audio["TITLE"] = info.title
|
||||
audio["ARTIST"] = info.artist
|
||||
audio["ALBUM"] = info.album
|
||||
audio["DATE"] = info.year
|
||||
audio["GENRE"] = info.genre
|
||||
if info.lyrics:
|
||||
audio["LYRICS"] = info.lyrics
|
||||
if info.picture:
|
||||
with open(info.picture, "rb") as img_file:
|
||||
image_data = img_file.read()
|
||||
audio.add_picture(image_data)
|
||||
|
||||
|
||||
def _set_mp4_tags(audio, info):
|
||||
audio["\xa9nam"] = info.title
|
||||
audio["\xa9ART"] = info.artist
|
||||
audio["\xa9alb"] = info.album
|
||||
audio["\xa9day"] = info.year
|
||||
audio["\xa9gen"] = info.genre
|
||||
if info.picture:
|
||||
with open(info.picture, "rb") as img_file:
|
||||
image_data = img_file.read()
|
||||
audio["covr"] = [image_data]
|
||||
|
||||
|
||||
def _set_ogg_tags(audio, info):
|
||||
audio["TITLE"] = info.title
|
||||
audio["ARTIST"] = info.artist
|
||||
audio["ALBUM"] = info.album
|
||||
audio["DATE"] = info.year
|
||||
audio["GENRE"] = info.genre
|
||||
if info.lyrics:
|
||||
audio["LYRICS"] = info.lyrics
|
||||
if info.picture:
|
||||
with open(info.picture, "rb") as img_file:
|
||||
image_data = img_file.read()
|
||||
audio["metadata_block_picture"] = base64.b64encode(image_data).decode()
|
||||
|
||||
|
||||
def _set_asf_tags(audio, info):
|
||||
audio["Title"] = info.title
|
||||
audio["Author"] = info.artist
|
||||
audio["WM/AlbumTitle"] = info.album
|
||||
audio["WM/Year"] = info.year
|
||||
audio["WM/Genre"] = info.genre
|
||||
if info.picture:
|
||||
with open(info.picture, "rb") as img_file:
|
||||
image_data = img_file.read()
|
||||
audio["WM/Picture"] = image_data
|
||||
|
||||
|
||||
def _set_wave_tags(audio, info):
|
||||
audio["Title"] = info.title
|
||||
audio["Artist"] = info.artist
|
||||
|
||||
|
||||
# 下载播放列表
|
||||
async def download_playlist(config, url, dirname):
|
||||
title = f"{dirname}/%(title)s.%(ext)s"
|
||||
@@ -731,6 +880,8 @@ async def download_playlist(config, url, dirname):
|
||||
"-x",
|
||||
"--audio-format",
|
||||
"mp3",
|
||||
"--audio-quality",
|
||||
"0",
|
||||
"--paths",
|
||||
config.download_path,
|
||||
"-o",
|
||||
@@ -764,6 +915,8 @@ async def download_one_music(config, url, name=""):
|
||||
"-x",
|
||||
"--audio-format",
|
||||
"mp3",
|
||||
"--audio-quality",
|
||||
"0",
|
||||
"--paths",
|
||||
config.download_path,
|
||||
"-o",
|
||||
@@ -812,6 +965,7 @@ def remove_common_prefix(directory):
|
||||
|
||||
log.info(f'Common prefix identified: "{common_prefix}"')
|
||||
|
||||
pattern = re.compile(r"^(\d+)\s+\d*(.+?)\.(.*$)")
|
||||
for filename in files:
|
||||
if filename == common_prefix:
|
||||
continue
|
||||
@@ -819,6 +973,12 @@ def remove_common_prefix(directory):
|
||||
if filename.startswith(common_prefix):
|
||||
# 构造新的文件名
|
||||
new_filename = filename[len(common_prefix) :]
|
||||
match = pattern.search(new_filename.strip())
|
||||
if match:
|
||||
num = match.group(1)
|
||||
name = match.group(2).replace(".", " ").strip()
|
||||
suffix = match.group(3)
|
||||
new_filename = f"{num}.{name}.{suffix}"
|
||||
# 生成完整的文件路径
|
||||
old_file_path = os.path.join(directory, filename)
|
||||
new_file_path = os.path.join(directory, new_filename)
|
||||
@@ -855,3 +1015,135 @@ def try_add_access_control_param(config, url):
|
||||
).geturl()
|
||||
|
||||
return new_url
|
||||
|
||||
|
||||
# 判断文件在不在排除目录列表
|
||||
def not_in_dirs(filename, ignore_absolute_dirs):
|
||||
file_absolute_path = os.path.abspath(filename)
|
||||
file_dir = os.path.dirname(file_absolute_path)
|
||||
for ignore_dir in ignore_absolute_dirs:
|
||||
if file_dir.startswith(ignore_dir):
|
||||
log.info(f"{file_dir} in {ignore_dir}")
|
||||
return False # 文件在排除目录中
|
||||
|
||||
return True # 文件不在排除目录中
|
||||
|
||||
|
||||
def is_docker():
|
||||
return os.path.exists("/app/.dockerenv")
|
||||
|
||||
|
||||
async def restart_xiaomusic():
|
||||
# 重启 xiaomusic 程序
|
||||
sbp_args = (
|
||||
"supervisorctl",
|
||||
"restart",
|
||||
"xiaomusic",
|
||||
)
|
||||
|
||||
cmd = " ".join(sbp_args)
|
||||
log.info(f"restart_xiaomusic: {cmd}")
|
||||
await asyncio.sleep(2)
|
||||
proc = await asyncio.create_subprocess_exec(*sbp_args)
|
||||
exit_code = await proc.wait() # 等待子进程完成
|
||||
log.info(f"restart_xiaomusic completed with exit code {exit_code}")
|
||||
return exit_code
|
||||
|
||||
|
||||
async def update_version(version: str, lite: bool = True):
|
||||
if not is_docker():
|
||||
ret = "xiaomusic 更新只能在 docker 中进行"
|
||||
log.info(ret)
|
||||
return ret
|
||||
lite_tag = ""
|
||||
if lite:
|
||||
lite_tag = "-lite"
|
||||
arch = get_os_architecture()
|
||||
if "unknown" in arch:
|
||||
log.warning(f"update_version failed: {arch}")
|
||||
return arch
|
||||
# https://github.com/hanxi/xiaomusic/releases/download/main/app-amd64-lite.tar.gz
|
||||
url = f"https://gproxy.hanxi.cc/proxy/hanxi/xiaomusic/releases/download/{version}/app-{arch}{lite_tag}.tar.gz"
|
||||
target_directory = "/app"
|
||||
return await download_and_extract(url, target_directory)
|
||||
|
||||
|
||||
def get_os_architecture():
|
||||
"""
|
||||
获取操作系统架构类型:amd64、arm64、arm-v7。
|
||||
|
||||
Returns:
|
||||
str: 架构类型
|
||||
"""
|
||||
arch = platform.machine().lower()
|
||||
|
||||
if arch in ("x86_64", "amd64"):
|
||||
return "amd64"
|
||||
elif arch in ("aarch64", "arm64"):
|
||||
return "arm64"
|
||||
elif "arm" in arch or "armv7" in arch:
|
||||
return "arm-v7"
|
||||
else:
|
||||
return f"unknown architecture: {arch}"
|
||||
|
||||
|
||||
async def download_and_extract(url: str, target_directory: str):
|
||||
ret = "OK"
|
||||
# 创建目标目录
|
||||
os.makedirs(target_directory, exist_ok=True)
|
||||
|
||||
# 使用 aiohttp 异步下载文件
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as response:
|
||||
if response.status == 200:
|
||||
file_name = os.path.join(target_directory, url.split("/")[-1])
|
||||
file_name = os.path.normpath(file_name)
|
||||
if not file_name.startswith(target_directory):
|
||||
log.warning(f"Invalid file path: {file_name}")
|
||||
return
|
||||
with open(file_name, "wb") as f:
|
||||
# 以块的方式下载文件,防止内存占用过大
|
||||
async for chunk in response.content.iter_any():
|
||||
f.write(chunk)
|
||||
log.info(f"文件下载完成: {file_name}")
|
||||
|
||||
# 解压下载的文件
|
||||
if file_name.endswith(".tar.gz"):
|
||||
await extract_tar_gz(file_name, target_directory)
|
||||
else:
|
||||
ret = f"下载失败, 包有问题: {file_name}"
|
||||
log.warning(ret)
|
||||
|
||||
else:
|
||||
ret = f"下载失败, 状态码: {response.status}"
|
||||
log.warning(ret)
|
||||
return ret
|
||||
|
||||
|
||||
async def extract_tar_gz(file_name: str, target_directory: str):
|
||||
# 使用 asyncio.create_subprocess_exec 执行 tar 解压命令
|
||||
command = ["tar", "-xzvf", file_name, "-C", target_directory]
|
||||
# 启动子进程执行解压命令
|
||||
await asyncio.create_subprocess_exec(*command)
|
||||
# 不等待子进程完成
|
||||
log.info(f"extract_tar_gz ing {file_name}")
|
||||
|
||||
|
||||
def chmodfile(file_path: str):
|
||||
try:
|
||||
os.chmod(file_path, 0o775)
|
||||
except Exception as e:
|
||||
log.info(f"chmodfile failed: {e}")
|
||||
|
||||
|
||||
def chmoddir(dir_path: str):
|
||||
# 获取指定目录下的所有文件和子目录
|
||||
for item in os.listdir(dir_path):
|
||||
item_path = os.path.join(dir_path, item)
|
||||
# 确保是文件,且不是目录
|
||||
if os.path.isfile(item_path):
|
||||
try:
|
||||
os.chmod(item_path, 0o775)
|
||||
log.info(f"Changed permissions of file: {item_path}")
|
||||
except Exception as e:
|
||||
log.info(f"chmoddir failed: {e}")
|
||||
|
||||
@@ -28,6 +28,7 @@ from xiaomusic.const import (
|
||||
COOKIE_TEMPLATE,
|
||||
GET_ASK_BY_MINA,
|
||||
LATEST_ASK_API,
|
||||
NEED_USE_PLAY_MUSIC_API,
|
||||
PLAY_TYPE_ALL,
|
||||
PLAY_TYPE_ONE,
|
||||
PLAY_TYPE_RND,
|
||||
@@ -40,6 +41,7 @@ from xiaomusic.plugin import PluginManager
|
||||
from xiaomusic.utils import (
|
||||
Metadata,
|
||||
chinese_to_number,
|
||||
chmodfile,
|
||||
custom_sort_key,
|
||||
deepcopy_data_no_sensitive_info,
|
||||
extract_audio_metadata,
|
||||
@@ -48,8 +50,11 @@ from xiaomusic.utils import (
|
||||
get_local_music_duration,
|
||||
get_web_music_duration,
|
||||
list2str,
|
||||
not_in_dirs,
|
||||
parse_cookie_string,
|
||||
parse_str_to_dict,
|
||||
save_picture_by_base64,
|
||||
set_music_tag_to_file,
|
||||
traverse_music_directory,
|
||||
try_add_access_control_param,
|
||||
)
|
||||
@@ -71,6 +76,7 @@ class XiaoMusic:
|
||||
self.all_music = {}
|
||||
self._all_radio = {} # 电台列表
|
||||
self.music_list = {} # 播放列表 key 为目录名, value 为 play_list
|
||||
self.default_music_list_names = [] # 非自定义个歌单
|
||||
self.devices = {} # key 为 did
|
||||
self.running_task = []
|
||||
self.all_music_tags = {} # 歌曲额外信息
|
||||
@@ -100,7 +106,7 @@ class XiaoMusic:
|
||||
self.update_devices()
|
||||
|
||||
# 启动统计
|
||||
self.analytics = Analytics(self.log)
|
||||
self.analytics = Analytics(self.log, self.config)
|
||||
|
||||
debug_config = deepcopy_data_no_sensitive_info(self.config)
|
||||
self.log.info(f"Startup OK. {debug_config}")
|
||||
@@ -156,7 +162,7 @@ class XiaoMusic:
|
||||
|
||||
log_file = self.config.log_file
|
||||
log_path = os.path.dirname(log_file)
|
||||
if not os.path.exists(log_path):
|
||||
if log_path and not os.path.exists(log_path):
|
||||
os.makedirs(log_path)
|
||||
if os.path.exists(log_file):
|
||||
os.remove(log_file)
|
||||
@@ -186,7 +192,7 @@ class XiaoMusic:
|
||||
self.last_timestamp[did] = int(time.time() * 1000)
|
||||
|
||||
hardware = self.get_hardward(device_id)
|
||||
if hardware in GET_ASK_BY_MINA or self.config.get_ask_by_mina:
|
||||
if (hardware in GET_ASK_BY_MINA) or self.config.get_ask_by_mina:
|
||||
tasks.append(self.get_latest_ask_by_mina(device_id))
|
||||
else:
|
||||
tasks.append(
|
||||
@@ -216,16 +222,19 @@ class XiaoMusic:
|
||||
self.cookie_jar = session.cookie_jar
|
||||
|
||||
async def login_miboy(self, session):
|
||||
account = MiAccount(
|
||||
session,
|
||||
self.config.account,
|
||||
self.config.password,
|
||||
str(self.mi_token_home),
|
||||
)
|
||||
# Forced login to refresh to refresh token
|
||||
await account.login("micoapi")
|
||||
self.mina_service = MiNAService(account)
|
||||
self.miio_service = MiIOService(account)
|
||||
try:
|
||||
account = MiAccount(
|
||||
session,
|
||||
self.config.account,
|
||||
self.config.password,
|
||||
str(self.mi_token_home),
|
||||
)
|
||||
# Forced login to refresh to refresh token
|
||||
await account.login("micoapi")
|
||||
self.mina_service = MiNAService(account)
|
||||
self.miio_service = MiIOService(account)
|
||||
except Exception as e:
|
||||
self.log.warning(f"可能登录失败. {e}")
|
||||
|
||||
async def try_update_device_id(self):
|
||||
try:
|
||||
@@ -245,11 +254,12 @@ class XiaoMusic:
|
||||
device.device_id = device_id
|
||||
device.hardware = hardware
|
||||
device.name = name
|
||||
device.play_type = PLAY_TYPE_RND
|
||||
devices[did] = device
|
||||
self.config.devices = devices
|
||||
self.log.info(f"选中的设备: {devices}")
|
||||
except Exception as e:
|
||||
self.log.exception(f"Execption {e}")
|
||||
self.log.warning(f"可能登录失败. {e}")
|
||||
|
||||
def get_cookie(self):
|
||||
if self.config.cookie:
|
||||
@@ -257,7 +267,7 @@ class XiaoMusic:
|
||||
return cookie_jar
|
||||
|
||||
if not os.path.exists(self.mi_token_home):
|
||||
self.log.error(f"{self.mi_token_home} file not exist")
|
||||
self.log.warning(f"{self.mi_token_home} file not exist")
|
||||
return None
|
||||
|
||||
with open(self.mi_token_home, encoding="utf-8") as f:
|
||||
@@ -314,19 +324,31 @@ class XiaoMusic:
|
||||
)
|
||||
# self.log.debug(f"url:{url} device_id:{device_id} hardware:{hardware}")
|
||||
r = await session.get(url, timeout=timeout, cookies=cookies)
|
||||
|
||||
# 检查响应状态码
|
||||
if r.status != 200:
|
||||
self.log.warning(f"Request failed with status {r.status}")
|
||||
continue
|
||||
|
||||
except asyncio.CancelledError:
|
||||
self.log.warning("Task was cancelled.")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
self.log.exception(f"Execption {e}")
|
||||
self.log.warning(f"Execption {e}")
|
||||
continue
|
||||
|
||||
try:
|
||||
data = await r.json()
|
||||
except Exception as e:
|
||||
self.log.exception(f"Execption {e}")
|
||||
self.log.warning(f"Execption {e}")
|
||||
if i == 2:
|
||||
# tricky way to fix #282 #272 # if it is the third time we re init all data
|
||||
self.log.info("Maybe outof date trying to re init it")
|
||||
await self.init_all_data(self.session)
|
||||
else:
|
||||
return self._get_last_query(device_id, data)
|
||||
self.log.warning("get_latest_ask_from_xiaoai. All retries failed.")
|
||||
|
||||
async def get_latest_ask_by_mina(self, device_id):
|
||||
try:
|
||||
@@ -357,7 +379,7 @@ class XiaoMusic:
|
||||
}
|
||||
self._check_last_query(last_record)
|
||||
except Exception as e:
|
||||
self.log.exception(f"get_latest_ask_by_mina {e}")
|
||||
self.log.warning(f"get_latest_ask_by_mina {e}")
|
||||
return
|
||||
|
||||
def _get_last_query(self, device_id, data):
|
||||
@@ -463,6 +485,29 @@ class XiaoMusic:
|
||||
)
|
||||
return tags
|
||||
|
||||
# 修改标签信息
|
||||
def set_music_tag(self, name, info):
|
||||
if self._tag_generation_task:
|
||||
self.log.info("tag 更新中,请等待")
|
||||
return "Tag generation task running"
|
||||
tags = copy.copy(self.all_music_tags.get(name, asdict(Metadata())))
|
||||
tags["title"] = info.title
|
||||
tags["artist"] = info.artist
|
||||
tags["album"] = info.album
|
||||
tags["year"] = info.year
|
||||
tags["genre"] = info.genre
|
||||
tags["lyrics"] = info.lyrics
|
||||
file_path = self.all_music[name]
|
||||
if info.picture:
|
||||
tags["picture"] = save_picture_by_base64(
|
||||
info.picture, self.config.picture_cache_path, file_path
|
||||
)
|
||||
if self.config.enable_save_tag and (not self.is_web_music(name)):
|
||||
set_music_tag_to_file(file_path, Metadata(tags))
|
||||
self.all_music_tags[name] = tags
|
||||
self.try_save_tag_cache()
|
||||
return "OK"
|
||||
|
||||
def get_music_url(self, name):
|
||||
if self.is_web_music(name):
|
||||
url = self.all_music[name]
|
||||
@@ -551,6 +596,9 @@ class XiaoMusic:
|
||||
|
||||
all_music_tags = self.try_load_from_tag_cache()
|
||||
all_music_tags.update(self.all_music_tags) # 保证最新
|
||||
|
||||
ignore_tag_absolute_dirs = self.config.get_ignore_tag_dirs()
|
||||
self.log.info(f"ignore_tag_absolute_dirs: {ignore_tag_absolute_dirs}")
|
||||
for name, file_or_url in only_items.items():
|
||||
start = time.perf_counter()
|
||||
if name not in all_music_tags:
|
||||
@@ -558,7 +606,9 @@ class XiaoMusic:
|
||||
if self.is_web_music(name):
|
||||
# TODO: 网络歌曲获取歌曲额外信息
|
||||
pass
|
||||
elif os.path.exists(file_or_url):
|
||||
elif os.path.exists(file_or_url) and not_in_dirs(
|
||||
file_or_url, ignore_tag_absolute_dirs
|
||||
):
|
||||
all_music_tags[name] = extract_audio_metadata(
|
||||
file_or_url, self.config.picture_cache_path
|
||||
)
|
||||
@@ -618,13 +668,15 @@ class XiaoMusic:
|
||||
"全部": [], # 包含所有歌曲和所有电台
|
||||
"下载": [], # 下载目录下的
|
||||
"其他": [], # 主目录下的
|
||||
"最近新增": [], # 按文件时间排序
|
||||
}
|
||||
)
|
||||
# 全部,所有,自定义歌单(收藏)
|
||||
self.music_list["全部"] = list(self.all_music.keys())
|
||||
self.music_list["所有歌曲"] = [
|
||||
name for name in self.all_music.keys() if name not in self._all_radio
|
||||
]
|
||||
# 最近新增(不包含网络歌单)
|
||||
self.music_list["最近新增"] = sorted(
|
||||
self.all_music.keys(),
|
||||
key=lambda x: os.path.getmtime(self.all_music[x]),
|
||||
reverse=True,
|
||||
)[: self.config.recently_added_playlist_len]
|
||||
|
||||
# 网络歌单
|
||||
try:
|
||||
@@ -633,6 +685,12 @@ class XiaoMusic:
|
||||
except Exception as e:
|
||||
self.log.exception(f"Execption {e}")
|
||||
|
||||
# 全部,所有,自定义歌单(收藏)
|
||||
self.music_list["全部"] = list(self.all_music.keys())
|
||||
self.music_list["所有歌曲"] = [
|
||||
name for name in self.all_music.keys() if name not in self._all_radio
|
||||
]
|
||||
|
||||
# 文件夹歌单
|
||||
for dir_name, musics in all_music_by_dir.items():
|
||||
self.music_list[dir_name] = list(musics.keys())
|
||||
@@ -642,6 +700,9 @@ class XiaoMusic:
|
||||
for _, play_list in self.music_list.items():
|
||||
play_list.sort(key=custom_sort_key)
|
||||
|
||||
# 非自定义个歌单
|
||||
self.default_music_list_names = list(self.music_list.keys())
|
||||
|
||||
# 刷新自定义歌单
|
||||
self.refresh_custom_play_list()
|
||||
|
||||
@@ -660,6 +721,11 @@ class XiaoMusic:
|
||||
|
||||
def refresh_custom_play_list(self):
|
||||
try:
|
||||
# 删除旧的自定义个歌单
|
||||
for k in list(self.music_list.keys()):
|
||||
if k not in self.default_music_list_names:
|
||||
del self.music_list[k]
|
||||
# 合并新的自定义个歌单
|
||||
custom_play_list = self.get_custom_play_list()
|
||||
for k, v in custom_play_list.items():
|
||||
self.music_list[k] = list(v)
|
||||
@@ -950,14 +1016,17 @@ class XiaoMusic:
|
||||
parts = arg1.split("|")
|
||||
list_name = parts[0]
|
||||
|
||||
music_name = ""
|
||||
if len(parts) > 1:
|
||||
music_name = parts[1]
|
||||
return await self.do_play_music_list(did, list_name, music_name)
|
||||
|
||||
async def do_play_music_list(self, did, list_name, music_name=""):
|
||||
list_name = self._find_real_music_list_name(list_name)
|
||||
if list_name not in self.music_list:
|
||||
await self.do_tts(did, f"播放列表{list_name}不存在")
|
||||
return
|
||||
|
||||
music_name = ""
|
||||
if len(parts) > 1:
|
||||
music_name = parts[1]
|
||||
await self.devices[did].play_music_list(list_name, music_name)
|
||||
|
||||
# 播放一个播放列表里第几个
|
||||
@@ -989,14 +1058,42 @@ class XiaoMusic:
|
||||
parts = arg1.split("|")
|
||||
search_key = parts[0]
|
||||
name = parts[1] if len(parts) > 1 else search_key
|
||||
if name == "":
|
||||
if not name:
|
||||
name = search_key
|
||||
|
||||
return await self.devices[did].play(name, search_key)
|
||||
# 语音播放会根据歌曲匹配更新当前播放列表
|
||||
return await self.do_play(
|
||||
did, name, search_key, exact=True, update_cur_list=True
|
||||
)
|
||||
|
||||
# 搜索播放:会产生临时播放列表
|
||||
async def search_play(self, did="", arg1="", **kwargs):
|
||||
parts = arg1.split("|")
|
||||
search_key = parts[0]
|
||||
name = parts[1] if len(parts) > 1 else search_key
|
||||
if not name:
|
||||
name = search_key
|
||||
|
||||
# 语音搜索播放会更新当前播放列表为临时播放列表
|
||||
return await self.do_play(
|
||||
did, name, search_key, exact=False, update_cur_list=False
|
||||
)
|
||||
|
||||
# 后台搜索播放
|
||||
async def do_play(
|
||||
self, did, name, search_key="", exact=False, update_cur_list=False
|
||||
):
|
||||
return await self.devices[did].play(name, search_key, exact, update_cur_list)
|
||||
|
||||
# 本地播放
|
||||
async def playlocal(self, did="", arg1="", **kwargs):
|
||||
return await self.devices[did].playlocal(arg1)
|
||||
return await self.devices[did].playlocal(arg1, update_cur_list=True)
|
||||
|
||||
# 本地搜索播放
|
||||
async def search_playlocal(self, did="", arg1="", **kwargs):
|
||||
return await self.devices[did].playlocal(
|
||||
arg1, exact=False, update_cur_list=False
|
||||
)
|
||||
|
||||
async def play_next(self, did="", **kwargs):
|
||||
return await self.devices[did].play_next()
|
||||
@@ -1016,18 +1113,22 @@ class XiaoMusic:
|
||||
# 添加歌曲到收藏列表
|
||||
async def add_to_favorites(self, did="", arg1="", **kwargs):
|
||||
name = arg1 if arg1 else self.playingmusic(did)
|
||||
self.log.info(f"add_to_favorites {name}")
|
||||
if not name:
|
||||
self.log.warning("当前没有在播放歌曲,添加歌曲到收藏列表失败")
|
||||
return
|
||||
|
||||
self.play_list_add_music("收藏", name)
|
||||
self.play_list_add_music("收藏", [name])
|
||||
|
||||
# 从收藏列表中移除
|
||||
async def del_from_favorites(self, did="", arg1="", **kwargs):
|
||||
name = arg1 if arg1 else self.playingmusic(did)
|
||||
self.log.info(f"del_from_favorites {name}")
|
||||
if not name:
|
||||
self.log.warning("当前没有在播放歌曲,从收藏列表中移除失败")
|
||||
return
|
||||
|
||||
self.play_list_del_music("收藏", name)
|
||||
self.play_list_del_music("收藏", [name])
|
||||
|
||||
# 更新每个设备的歌单
|
||||
def update_all_playlist(self):
|
||||
@@ -1067,11 +1168,57 @@ class XiaoMusic:
|
||||
self.save_custom_play_list()
|
||||
return True
|
||||
|
||||
# 修改歌单名字
|
||||
def play_list_update_name(self, oldname, newname):
|
||||
custom_play_list = self.get_custom_play_list()
|
||||
if oldname not in custom_play_list:
|
||||
self.log.info(f"旧歌单名字不存在 {oldname}")
|
||||
return False
|
||||
if newname in custom_play_list:
|
||||
self.log.info(f"新歌单名字已存在 {newname}")
|
||||
return False
|
||||
play_list = custom_play_list[oldname]
|
||||
custom_play_list.pop(oldname)
|
||||
custom_play_list[newname] = play_list
|
||||
self.save_custom_play_list()
|
||||
return True
|
||||
|
||||
# 获取所有自定义歌单
|
||||
def get_play_list_names(self):
|
||||
custom_play_list = self.get_custom_play_list()
|
||||
return list(custom_play_list.keys())
|
||||
|
||||
# 获取歌单中所有歌曲
|
||||
def play_list_musics(self, name):
|
||||
custom_play_list = self.get_custom_play_list()
|
||||
if name not in custom_play_list:
|
||||
return "歌单不存在", []
|
||||
play_list = custom_play_list[name]
|
||||
return "OK", play_list
|
||||
|
||||
# 歌单更新歌曲
|
||||
def play_list_update_music(self, name, music_list):
|
||||
custom_play_list = self.get_custom_play_list()
|
||||
if name not in custom_play_list:
|
||||
# 歌单不存在则新建
|
||||
if not self.play_list_add(name):
|
||||
return False
|
||||
play_list = []
|
||||
for music_name in music_list:
|
||||
if (music_name in self.all_music) and (music_name not in play_list):
|
||||
play_list.append(music_name)
|
||||
# 直接覆盖
|
||||
custom_play_list[name] = play_list
|
||||
self.save_custom_play_list()
|
||||
return True
|
||||
|
||||
# 歌单新增歌曲
|
||||
def play_list_add_music(self, name, music_list):
|
||||
custom_play_list = self.get_custom_play_list()
|
||||
if name not in custom_play_list:
|
||||
return False
|
||||
# 歌单不存在则新建
|
||||
if not self.play_list_add(name):
|
||||
return False
|
||||
play_list = custom_play_list[name]
|
||||
for music_name in music_list:
|
||||
if (music_name in self.all_music) and (music_name not in play_list):
|
||||
@@ -1087,7 +1234,7 @@ class XiaoMusic:
|
||||
play_list = custom_play_list[name]
|
||||
for music_name in music_list:
|
||||
if music_name in play_list:
|
||||
play_list.pop(music_name)
|
||||
play_list.remove(music_name)
|
||||
self.save_custom_play_list()
|
||||
return True
|
||||
|
||||
@@ -1203,7 +1350,7 @@ class XiaoMusic:
|
||||
try:
|
||||
device_list = await self.mina_service.device_list()
|
||||
except Exception as e:
|
||||
self.log.exception(f"Execption {e}")
|
||||
self.log.warning(f"Execption {e}")
|
||||
return device_list
|
||||
|
||||
async def debug_play_by_music_url(self, arg1=None):
|
||||
@@ -1250,6 +1397,7 @@ class XiaoMusicDevice:
|
||||
self._start_time = 0
|
||||
self._duration = 0
|
||||
self._paused_time = 0
|
||||
self._play_failed_cnt = 0
|
||||
|
||||
self._play_list = []
|
||||
|
||||
@@ -1260,7 +1408,11 @@ class XiaoMusicDevice:
|
||||
|
||||
@property
|
||||
def did(self):
|
||||
return self.xiaomusic.device_id_did[self.device_id]
|
||||
return self.device.did
|
||||
|
||||
@property
|
||||
def hardware(self):
|
||||
return self.device.hardware
|
||||
|
||||
def get_cur_music(self):
|
||||
return self.device.cur_music
|
||||
@@ -1305,11 +1457,16 @@ class XiaoMusicDevice:
|
||||
)
|
||||
|
||||
# 播放歌曲
|
||||
async def play(self, name="", search_key=""):
|
||||
async def play(self, name="", search_key="", exact=True, update_cur_list=False):
|
||||
self._last_cmd = "play"
|
||||
return await self._play(name=name, search_key=search_key, update_cur=True)
|
||||
return await self._play(
|
||||
name=name,
|
||||
search_key=search_key,
|
||||
exact=exact,
|
||||
update_cur_list=update_cur_list,
|
||||
)
|
||||
|
||||
async def _play(self, name="", search_key="", exact=False, update_cur=False):
|
||||
async def _play(self, name="", search_key="", exact=True, update_cur_list=False):
|
||||
if search_key == "" and name == "":
|
||||
if self.check_play_next():
|
||||
await self._play_next()
|
||||
@@ -1324,15 +1481,20 @@ class XiaoMusicDevice:
|
||||
else:
|
||||
names = self.xiaomusic.find_real_music_name(name)
|
||||
if len(names) > 0:
|
||||
if update_cur and len(names) > 1: # 大于一首歌才更新
|
||||
self._play_list = names
|
||||
self.device.cur_playlist = "临时搜索列表"
|
||||
self.update_playlist()
|
||||
elif update_cur: # 只有一首歌,append
|
||||
self._play_list = self._play_list + names
|
||||
self.device.cur_playlist = "临时搜索列表"
|
||||
self.update_playlist(reorder=False)
|
||||
if not exact:
|
||||
if len(names) > 1: # 大于一首歌才更新
|
||||
self._play_list = names
|
||||
self.device.cur_playlist = "临时搜索列表"
|
||||
self.update_playlist()
|
||||
else: # 只有一首歌,append
|
||||
self._play_list = self._play_list + names
|
||||
self.device.cur_playlist = "临时搜索列表"
|
||||
self.update_playlist(reorder=False)
|
||||
name = names[0]
|
||||
if update_cur_list:
|
||||
# 根据当前歌曲匹配歌曲列表
|
||||
self.device.cur_playlist = self.find_cur_playlist(name)
|
||||
self.update_playlist()
|
||||
self.log.debug(
|
||||
f"当前播放列表为:{list2str(self._play_list, self.config.verbose)}"
|
||||
)
|
||||
@@ -1341,8 +1503,6 @@ class XiaoMusicDevice:
|
||||
await self.do_tts(f"本地不存在歌曲{name}")
|
||||
return
|
||||
await self.download(search_key, name)
|
||||
self.log.info(f"正在下载中 {search_key} {name}")
|
||||
await self._download_proc.wait()
|
||||
# 把文件插入到播放列表里
|
||||
await self.add_download_music(name)
|
||||
await self._playmusic(name)
|
||||
@@ -1391,7 +1551,7 @@ class XiaoMusicDevice:
|
||||
await self._play(name, exact=True)
|
||||
|
||||
# 播放本地歌曲
|
||||
async def playlocal(self, name):
|
||||
async def playlocal(self, name, exact=True, update_cur_list=False):
|
||||
self._last_cmd = "playlocal"
|
||||
if name == "":
|
||||
if self.check_play_next():
|
||||
@@ -1403,17 +1563,25 @@ class XiaoMusicDevice:
|
||||
self.log.info(f"playlocal. name:{name}")
|
||||
|
||||
# 本地歌曲不存在时下载
|
||||
names = self.xiaomusic.find_real_music_name(name)
|
||||
if exact:
|
||||
names = self.xiaomusic.find_real_music_name(name, n=1)
|
||||
else:
|
||||
names = self.xiaomusic.find_real_music_name(name)
|
||||
if len(names) > 0:
|
||||
if len(names) > 1: # 大于一首歌才更新
|
||||
self._play_list = names
|
||||
self.device.cur_playlist = "临时搜索列表"
|
||||
self.update_playlist()
|
||||
else: # 只有一首歌,append
|
||||
self._play_list = self._play_list + names
|
||||
self.device.cur_playlist = "临时搜索列表"
|
||||
self.update_playlist(reorder=False)
|
||||
if not exact:
|
||||
if len(names) > 1: # 大于一首歌才更新
|
||||
self._play_list = names
|
||||
self.device.cur_playlist = "临时搜索列表"
|
||||
self.update_playlist()
|
||||
else: # 只有一首歌,append
|
||||
self._play_list = self._play_list + names
|
||||
self.device.cur_playlist = "临时搜索列表"
|
||||
self.update_playlist(reorder=False)
|
||||
name = names[0]
|
||||
if update_cur_list:
|
||||
# 根据当前歌曲匹配歌曲列表
|
||||
self.device.cur_playlist = self.find_cur_playlist(name)
|
||||
self.update_playlist()
|
||||
self.log.debug(
|
||||
f"当前播放列表为:{list2str(self._play_list, self.config.verbose)}"
|
||||
)
|
||||
@@ -1435,14 +1603,21 @@ class XiaoMusicDevice:
|
||||
self.log.info(f"播放 {url}")
|
||||
results = await self.group_player_play(url, name)
|
||||
if all(ele is None for ele in results):
|
||||
self.log.info(f"播放 {name} 失败")
|
||||
self.log.info(f"播放 {name} 失败. 失败次数: {self._play_failed_cnt}")
|
||||
await asyncio.sleep(1)
|
||||
if self.isplaying() and self._last_cmd != "stop":
|
||||
if (
|
||||
self.isplaying()
|
||||
and self._last_cmd != "stop"
|
||||
and self._play_failed_cnt < 10
|
||||
):
|
||||
self._play_failed_cnt = self._play_failed_cnt + 1
|
||||
await self._play_next()
|
||||
return
|
||||
# 重置播放失败次数
|
||||
self._play_failed_cnt = 0
|
||||
|
||||
self.log.info(f"【{name}】已经开始播放了")
|
||||
await self.xiaomusic.analytics.send_play_event(name, sec)
|
||||
await self.xiaomusic.analytics.send_play_event(name, sec, self.hardware)
|
||||
|
||||
if self.device.play_type == PLAY_TYPE_SIN:
|
||||
self.log.info(f"【{name}】单曲播放时不会设置下一首歌的定时器")
|
||||
@@ -1482,7 +1657,7 @@ class XiaoMusicDevice:
|
||||
)
|
||||
await self.stop_if_xiaoai_is_playing(device_id)
|
||||
except Exception as e:
|
||||
self.log.exception(f"Execption {e}")
|
||||
self.log.warning(f"Execption {e}")
|
||||
|
||||
async def get_if_xiaoai_is_playing(self):
|
||||
playing_info = await self.xiaomusic.mina_service.player_get_status(
|
||||
@@ -1533,6 +1708,8 @@ class XiaoMusicDevice:
|
||||
"-x",
|
||||
"--audio-format",
|
||||
"mp3",
|
||||
"--audio-quality",
|
||||
"0",
|
||||
"--paths",
|
||||
self.download_path,
|
||||
"-o",
|
||||
@@ -1552,6 +1729,11 @@ class XiaoMusicDevice:
|
||||
self.log.info(f"download cmd: {cmd}")
|
||||
self._download_proc = await asyncio.create_subprocess_exec(*sbp_args)
|
||||
await self.do_tts(f"正在下载歌曲{search_key}")
|
||||
self.log.info(f"正在下载中 {search_key} {name}")
|
||||
await self._download_proc.wait()
|
||||
# 下载完成后,修改文件权限
|
||||
file_path = os.path.join(self.download_path, f"{name}.mp3")
|
||||
chmodfile(file_path)
|
||||
|
||||
# 继续播放被打断的歌曲
|
||||
async def check_replay(self):
|
||||
@@ -1686,7 +1868,9 @@ class XiaoMusicDevice:
|
||||
self.log.info(
|
||||
f"play_one_url continue_play device_id:{device_id} ret:{ret} url:{url} audio_id:{audio_id}"
|
||||
)
|
||||
elif self.config.use_music_api:
|
||||
elif self.config.use_music_api or (
|
||||
self.hardware in NEED_USE_PLAY_MUSIC_API
|
||||
):
|
||||
ret = await self.xiaomusic.mina_service.play_by_music_url(
|
||||
device_id, url, audio_id=audio_id
|
||||
)
|
||||
@@ -1703,7 +1887,7 @@ class XiaoMusicDevice:
|
||||
return ret
|
||||
|
||||
async def _get_audio_id(self, name):
|
||||
audio_id = 1582971365183456177
|
||||
audio_id = self.config.use_music_audio_id or "1582971365183456177"
|
||||
if not (self.config.use_music_api or self.config.continue_play):
|
||||
return str(audio_id)
|
||||
try:
|
||||
@@ -1770,13 +1954,17 @@ class XiaoMusicDevice:
|
||||
self.log.exception(f"Execption {e}")
|
||||
|
||||
async def get_volume(self):
|
||||
playing_info = await self.xiaomusic.mina_service.player_get_status(
|
||||
self.device_id
|
||||
)
|
||||
self.log.info(f"get_volume. playing_info:{playing_info}")
|
||||
volume = json.loads(playing_info.get("data", {}).get("info", "{}")).get(
|
||||
"volume", 0
|
||||
)
|
||||
volume = 0
|
||||
try:
|
||||
playing_info = await self.xiaomusic.mina_service.player_get_status(
|
||||
self.device_id
|
||||
)
|
||||
self.log.info(f"get_volume. playing_info:{playing_info}")
|
||||
volume = json.loads(playing_info.get("data", {}).get("info", "{}")).get(
|
||||
"volume", 0
|
||||
)
|
||||
except Exception as e:
|
||||
self.log.warning(f"Execption {e}")
|
||||
volume = int(volume)
|
||||
self.log.info("get_volume. volume:%d", volume)
|
||||
return volume
|
||||
@@ -1793,7 +1981,7 @@ class XiaoMusicDevice:
|
||||
self._last_cmd = "play_music_list"
|
||||
self.device.cur_playlist = list_name
|
||||
self.update_playlist()
|
||||
self.log.info(f"开始播放列表{list_name}")
|
||||
self.log.info(f"开始播放列表{list_name} {music_name}")
|
||||
await self._play(music_name, exact=True)
|
||||
|
||||
async def stop(self, arg1=""):
|
||||
@@ -1867,3 +2055,27 @@ class XiaoMusicDevice:
|
||||
for key in list(d):
|
||||
val = d.pop(key)
|
||||
val.cancel_all_timer()
|
||||
|
||||
# 根据当前歌曲匹配歌曲列表
|
||||
def find_cur_playlist(self, name):
|
||||
# 匹配顺序:
|
||||
# 1. 收藏
|
||||
# 2. 最近新增
|
||||
# 3. 排除(全部,所有歌曲,所有电台,临时搜索列表)
|
||||
# 4. 所有歌曲
|
||||
# 5. 所有电台
|
||||
# 6. 全部
|
||||
if name in self.xiaomusic.music_list.get("收藏", []):
|
||||
return "收藏"
|
||||
if name in self.xiaomusic.music_list.get("最近新增", []):
|
||||
return "最近新增"
|
||||
for list_name, play_list in self.xiaomusic.music_list.items():
|
||||
if (list_name not in ["全部", "所有歌曲", "所有电台", "临时搜索列表"]) and (
|
||||
name in play_list
|
||||
):
|
||||
return list_name
|
||||
if name in self.xiaomusic.music_list.get("所有歌曲", []):
|
||||
return "所有歌曲"
|
||||
if name in self.xiaomusic.music_list.get("所有电台", []):
|
||||
return "所有电台"
|
||||
return "全部"
|
||||
|
||||
Reference in New Issue
Block a user