Merge branch 'dev'
This commit is contained in:
commit
6dbd3f4d6e
|
@ -128,4 +128,8 @@ dmypy.json
|
||||||
# Pyre type checker
|
# Pyre type checker
|
||||||
.pyre/
|
.pyre/
|
||||||
|
|
||||||
.vscode/
|
<<<<<<< HEAD
|
||||||
|
.vscode/
|
||||||
|
=======
|
||||||
|
.vscode/
|
||||||
|
>>>>>>> dev
|
||||||
|
|
19
README.md
19
README.md
|
@ -26,6 +26,8 @@ https://github.com/Binaryify/NeteaseCloudMusicApi
|
||||||
|
|
||||||
不想动手不想操心,也可以付费使用由我部署维护的接口服务(每年30)
|
不想动手不想操心,也可以付费使用由我部署维护的接口服务(每年30)
|
||||||
|
|
||||||
|
**注意:关联媒体播放器调整为在集成选项中选择**
|
||||||
|
|
||||||
## 使用 - [插件图片预览](https://github.com/shaonianzhentan/image/blob/main/ha_cloud_music/README.md)
|
## 使用 - [插件图片预览](https://github.com/shaonianzhentan/image/blob/main/ha_cloud_music/README.md)
|
||||||
|
|
||||||
> **指定ID播放**
|
> **指定ID播放**
|
||||||
|
@ -47,21 +49,10 @@ https://github.com/Binaryify/NeteaseCloudMusicApi
|
||||||
|
|
||||||
> **登录后播放**
|
> **登录后播放**
|
||||||
- [x] 每日推荐 `cloudmusic://163/my/daily`
|
- [x] 每日推荐 `cloudmusic://163/my/daily`
|
||||||
|
- [x] 我喜欢的音乐 `cloudmusic://163/my/ilike`
|
||||||
|
|
||||||
|
|
||||||
configuration.yaml
|
|
||||||
```yaml
|
|
||||||
homeassistant:
|
|
||||||
customize: !include customize.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
customize.yaml
|
|
||||||
```yaml
|
|
||||||
media_player.yun_yin_le:
|
|
||||||
media_player:
|
|
||||||
- media_player.源实体1
|
|
||||||
- media_player.源实体2
|
|
||||||
- media_player.源实体3
|
|
||||||
```
|
|
||||||
|
|
||||||
## 关联项目
|
## 关联项目
|
||||||
|
|
||||||
|
@ -74,4 +65,4 @@ media_player.yun_yin_le:
|
||||||
<img src="https://ha.jiluxinqing.com/img/alipay.png" align="left" height="160" width="160" alt="支付宝" title="支付宝"> | <img src="https://ha.jiluxinqing.com/img/wechat.png" align="left" height="160" width="160" alt="微信支付" title="微信">
|
<img src="https://ha.jiluxinqing.com/img/alipay.png" align="left" height="160" width="160" alt="支付宝" title="支付宝"> | <img src="https://ha.jiluxinqing.com/img/wechat.png" align="left" height="160" width="160" alt="微信支付" title="微信">
|
||||||
|
|
||||||
#### 关注我的微信订阅号,了解更多HomeAssistant相关知识
|
#### 关注我的微信订阅号,了解更多HomeAssistant相关知识
|
||||||
<img src="https://ha.jiluxinqing.com/img/wechat-channel.png" height="160" alt="HomeAssistant家庭助理" title="HomeAssistant家庭助理">
|
<img src="https://ha.jiluxinqing.com/img/wechat-channel.png" height="160" alt="HomeAssistant家庭助理" title="HomeAssistant家庭助理">
|
|
@ -86,6 +86,7 @@ class CloudMusicRouter():
|
||||||
|
|
||||||
my_login = f'{cloudmusic_protocol}my/login'
|
my_login = f'{cloudmusic_protocol}my/login'
|
||||||
my_daily = f'{cloudmusic_protocol}my/daily'
|
my_daily = f'{cloudmusic_protocol}my/daily'
|
||||||
|
my_ilike = f'{cloudmusic_protocol}my/ilike'
|
||||||
my_recommend_resource = f'{cloudmusic_protocol}my/recommend_resource'
|
my_recommend_resource = f'{cloudmusic_protocol}my/recommend_resource'
|
||||||
my_cloud = f'{cloudmusic_protocol}my/cloud'
|
my_cloud = f'{cloudmusic_protocol}my/cloud'
|
||||||
my_created = f'{cloudmusic_protocol}my/created'
|
my_created = f'{cloudmusic_protocol}my/created'
|
||||||
|
@ -762,6 +763,8 @@ async def async_play_media(media_player, cloud_music, media_content_id):
|
||||||
playlist = await cloud_music.async_get_playlist(id)
|
playlist = await cloud_music.async_get_playlist(id)
|
||||||
elif media_content_id.startswith(CloudMusicRouter.my_daily):
|
elif media_content_id.startswith(CloudMusicRouter.my_daily):
|
||||||
playlist = await cloud_music.async_get_dailySongs()
|
playlist = await cloud_music.async_get_dailySongs()
|
||||||
|
elif media_content_id.startswith(CloudMusicRouter.my_ilike):
|
||||||
|
playlist = await cloud_music.async_get_ilinkSongs()
|
||||||
elif media_content_id.startswith(CloudMusicRouter.my_cloud):
|
elif media_content_id.startswith(CloudMusicRouter.my_cloud):
|
||||||
playlist = await cloud_music.async_get_cloud()
|
playlist = await cloud_music.async_get_cloud()
|
||||||
elif media_content_id.startswith(CloudMusicRouter.artist_playlist):
|
elif media_content_id.startswith(CloudMusicRouter.artist_playlist):
|
||||||
|
|
|
@ -258,6 +258,13 @@ class CloudMusic():
|
||||||
|
|
||||||
return list(map(format_playlist, res['data']['dailySongs']))
|
return list(map(format_playlist, res['data']['dailySongs']))
|
||||||
|
|
||||||
|
# 获取我喜欢的音乐
|
||||||
|
async def async_get_ilinkSongs(self):
|
||||||
|
uid = self.userinfo.get('uid')
|
||||||
|
if uid is not None:
|
||||||
|
res = await self.netease_cloud_music(f'/user/playlist?uid={uid}')
|
||||||
|
return await self.async_get_playlist(res['playlist'][0]['id'])
|
||||||
|
|
||||||
# 乐听头条
|
# 乐听头条
|
||||||
async def async_ting_playlist(self, catalog_id):
|
async def async_ting_playlist(self, catalog_id):
|
||||||
|
|
||||||
|
|
|
@ -12,14 +12,14 @@ from homeassistant.core import callback
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from .manifest import manifest
|
from .manifest import manifest
|
||||||
from .http_api import http_cookie
|
from .http_api import fetch_data
|
||||||
from homeassistant.util.json import save_json
|
from homeassistant.util.json import save_json
|
||||||
|
|
||||||
DOMAIN = manifest.domain
|
DOMAIN = manifest.domain
|
||||||
|
|
||||||
class SimpleConfigFlow(ConfigFlow, domain=DOMAIN):
|
class SimpleConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 2
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
@ -30,8 +30,15 @@ class SimpleConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
errors = {}
|
errors = {}
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
url = user_input.get(CONF_URL).strip('/')
|
url = user_input.get(CONF_URL).strip('/')
|
||||||
user_input[CONF_URL] = url
|
# 检查接口是否可用
|
||||||
return self.async_create_entry(title=DOMAIN, data=user_input)
|
try:
|
||||||
|
res = await fetch_data(f'{url}/login/status')
|
||||||
|
if res['data']['code'] == 200:
|
||||||
|
user_input[CONF_URL] = url
|
||||||
|
return self.async_create_entry(title=DOMAIN, data=user_input)
|
||||||
|
except Exception as ex:
|
||||||
|
print(ex)
|
||||||
|
errors = { 'base': 'api_failed' }
|
||||||
else:
|
else:
|
||||||
user_input = {}
|
user_input = {}
|
||||||
|
|
||||||
|
@ -45,7 +52,6 @@ class SimpleConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
def async_get_options_flow(entry: ConfigEntry):
|
def async_get_options_flow(entry: ConfigEntry):
|
||||||
return OptionsFlowHandler(entry)
|
return OptionsFlowHandler(entry)
|
||||||
|
|
||||||
|
|
||||||
class OptionsFlowHandler(OptionsFlow):
|
class OptionsFlowHandler(OptionsFlow):
|
||||||
def __init__(self, config_entry: ConfigEntry):
|
def __init__(self, config_entry: ConfigEntry):
|
||||||
self.config_entry = config_entry
|
self.config_entry = config_entry
|
||||||
|
@ -57,20 +63,22 @@ class OptionsFlowHandler(OptionsFlow):
|
||||||
options = self.config_entry.options
|
options = self.config_entry.options
|
||||||
errors = {}
|
errors = {}
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
username = user_input.get(CONF_USERNAME)
|
return self.async_create_entry(title='', data=user_input)
|
||||||
password = user_input.get(CONF_PASSWORD)
|
|
||||||
|
media_states = self.hass.states.async_all('media_player')
|
||||||
|
media_entities = {}
|
||||||
|
|
||||||
cloud_music = self.hass.data['cloud_music']
|
for state in media_states:
|
||||||
|
friendly_name = state.attributes.get('friendly_name')
|
||||||
|
platform = state.attributes.get('platform')
|
||||||
|
entity_id = state.entity_id
|
||||||
|
value = f'{friendly_name}({entity_id})'
|
||||||
|
|
||||||
result = await cloud_music.login(username, password)
|
if platform != 'cloud_music':
|
||||||
if result is not None:
|
media_entities[entity_id] = value
|
||||||
return self.async_create_entry(title='', data=user_input)
|
|
||||||
else:
|
|
||||||
errors['base'] = 'login_failed'
|
|
||||||
|
|
||||||
DATA_SCHEMA = vol.Schema({
|
DATA_SCHEMA = vol.Schema({
|
||||||
vol.Required(CONF_USERNAME, default=options.get(CONF_USERNAME)): str,
|
vol.Required('media_player', default=options.get('media_player')): vol.In(media_entities)
|
||||||
vol.Required(CONF_PASSWORD, default=options.get(CONF_PASSWORD)): str
|
|
||||||
})
|
})
|
||||||
return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA, errors=errors)
|
return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA, errors=errors)
|
||||||
|
|
|
@ -24,7 +24,6 @@ async def http_cookie(url):
|
||||||
}
|
}
|
||||||
|
|
||||||
async def http_get(url, COOKIES={}):
|
async def http_get(url, COOKIES={}):
|
||||||
print(url)
|
|
||||||
headers = {'Referer': url, **HEADERS}
|
headers = {'Referer': url, **HEADERS}
|
||||||
jar = aiohttp.CookieJar(unsafe=True)
|
jar = aiohttp.CookieJar(unsafe=True)
|
||||||
async with aiohttp.ClientSession(headers=headers, cookies=COOKIES, cookie_jar=jar) as session:
|
async with aiohttp.ClientSession(headers=headers, cookies=COOKIES, cookie_jar=jar) as session:
|
||||||
|
@ -39,4 +38,10 @@ async def http_get(url, COOKIES={}):
|
||||||
async def http_code(url):
|
async def http_code(url):
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession() as session:
|
||||||
async with session.get(url) as response:
|
async with session.get(url) as response:
|
||||||
return response.status
|
return response.status
|
||||||
|
|
||||||
|
async def fetch_data(url):
|
||||||
|
timeout = aiohttp.ClientTimeout(total=5)
|
||||||
|
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||||
|
async with session.get(url) as response:
|
||||||
|
return await response.json()
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"domain": "ha_cloud_music",
|
"domain": "ha_cloud_music",
|
||||||
"name": "\u4E91\u97F3\u4E50",
|
"name": "\u4E91\u97F3\u4E50",
|
||||||
"version": "2023.8.13",
|
"version": "2023.11.1",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://github.com/shaonianzhentan/ha_cloud_music",
|
"documentation": "https://github.com/shaonianzhentan/ha_cloud_music",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
|
|
|
@ -13,7 +13,6 @@ from homeassistant.components.media_player.const import (
|
||||||
SUPPORT_VOLUME_SET,
|
SUPPORT_VOLUME_SET,
|
||||||
SUPPORT_VOLUME_MUTE,
|
SUPPORT_VOLUME_MUTE,
|
||||||
SUPPORT_SELECT_SOURCE,
|
SUPPORT_SELECT_SOURCE,
|
||||||
SUPPORT_SELECT_SOUND_MODE,
|
|
||||||
SUPPORT_PLAY_MEDIA,
|
SUPPORT_PLAY_MEDIA,
|
||||||
SUPPORT_PLAY,
|
SUPPORT_PLAY,
|
||||||
SUPPORT_PAUSE,
|
SUPPORT_PAUSE,
|
||||||
|
@ -51,7 +50,6 @@ DOMAIN = manifest.domain
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
SUPPORT_FEATURES = SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | \
|
SUPPORT_FEATURES = SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET | \
|
||||||
SUPPORT_SELECT_SOURCE | SUPPORT_SELECT_SOUND_MODE | \
|
|
||||||
SUPPORT_PLAY_MEDIA | SUPPORT_PLAY | SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
|
SUPPORT_PLAY_MEDIA | SUPPORT_PLAY | SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
|
||||||
SUPPORT_BROWSE_MEDIA | SUPPORT_SEEK | SUPPORT_CLEAR_PLAYLIST | SUPPORT_SHUFFLE_SET | SUPPORT_REPEAT_SET
|
SUPPORT_BROWSE_MEDIA | SUPPORT_SEEK | SUPPORT_CLEAR_PLAYLIST | SUPPORT_SHUFFLE_SET | SUPPORT_REPEAT_SET
|
||||||
|
|
||||||
|
@ -63,14 +61,14 @@ async def async_setup_entry(
|
||||||
entry: ConfigEntry,
|
entry: ConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
media_player = CloudMusicMediaPlayer(hass)
|
media_player = CloudMusicMediaPlayer(hass, entry)
|
||||||
|
|
||||||
await hass.async_add_executor_job(track_time_interval, hass, media_player.interval, TIME_BETWEEN_UPDATES)
|
await hass.async_add_executor_job(track_time_interval, hass, media_player.interval, TIME_BETWEEN_UPDATES)
|
||||||
async_add_entities([ media_player ], True)
|
async_add_entities([ media_player ], True)
|
||||||
|
|
||||||
class CloudMusicMediaPlayer(MediaPlayerEntity):
|
class CloudMusicMediaPlayer(MediaPlayerEntity):
|
||||||
|
|
||||||
def __init__(self, hass):
|
def __init__(self, hass, entry):
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self._attributes = {
|
self._attributes = {
|
||||||
'platform': 'cloud_music'
|
'platform': 'cloud_music'
|
||||||
|
@ -81,9 +79,7 @@ class CloudMusicMediaPlayer(MediaPlayerEntity):
|
||||||
self._attr_supported_features = SUPPORT_FEATURES
|
self._attr_supported_features = SUPPORT_FEATURES
|
||||||
|
|
||||||
# default attribute
|
# default attribute
|
||||||
self._attr_source_list = []
|
self.source_media_player = entry.options.get('media_player')
|
||||||
self._attr_sound_mode = None
|
|
||||||
self._attr_sound_mode_list = []
|
|
||||||
self._attr_name = manifest.name
|
self._attr_name = manifest.name
|
||||||
self._attr_unique_id = manifest.documentation
|
self._attr_unique_id = manifest.documentation
|
||||||
self._attr_state = STATE_ON
|
self._attr_state = STATE_ON
|
||||||
|
@ -140,8 +136,8 @@ class CloudMusicMediaPlayer(MediaPlayerEntity):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_player(self):
|
def media_player(self):
|
||||||
if self.entity_id is not None and self._attr_sound_mode is not None:
|
if self.entity_id is not None and self.source_media_player is not None:
|
||||||
return self.hass.states.get(self._attr_sound_mode)
|
return self.hass.states.get(self.source_media_player)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_info(self):
|
def device_info(self):
|
||||||
|
@ -162,15 +158,6 @@ class CloudMusicMediaPlayer(MediaPlayerEntity):
|
||||||
async def async_browse_media(self, media_content_type=None, media_content_id=None):
|
async def async_browse_media(self, media_content_type=None, media_content_id=None):
|
||||||
return await self.cloud_music.async_browse_media(self, media_content_type, media_content_id)
|
return await self.cloud_music.async_browse_media(self, media_content_type, media_content_id)
|
||||||
|
|
||||||
async def async_select_source(self, source):
|
|
||||||
if self._attr_source_list.count(source) > 0:
|
|
||||||
self._attr_source = source
|
|
||||||
|
|
||||||
async def async_select_sound_mode(self, sound_mode):
|
|
||||||
if self._attr_sound_mode_list.count(sound_mode) > 0:
|
|
||||||
await self.async_media_pause()
|
|
||||||
self._attr_sound_mode = sound_mode
|
|
||||||
|
|
||||||
async def async_volume_up(self):
|
async def async_volume_up(self):
|
||||||
await self.async_call('volume_up')
|
await self.async_call('volume_up')
|
||||||
|
|
||||||
|
@ -240,18 +227,7 @@ class CloudMusicMediaPlayer(MediaPlayerEntity):
|
||||||
|
|
||||||
# 更新属性
|
# 更新属性
|
||||||
async def async_update(self):
|
async def async_update(self):
|
||||||
if self.entity_id is not None:
|
pass
|
||||||
state = self.hass.states.get(self.entity_id)
|
|
||||||
entities = state.attributes.get('media_player')
|
|
||||||
if entities is not None:
|
|
||||||
# 兼容初版
|
|
||||||
if isinstance(entities, str):
|
|
||||||
entities = [ entities ]
|
|
||||||
|
|
||||||
if len(entities) > 0:
|
|
||||||
self._attr_sound_mode_list = entities
|
|
||||||
if self._attr_sound_mode is None:
|
|
||||||
self._attr_sound_mode = entities[0]
|
|
||||||
|
|
||||||
# 调用服务
|
# 调用服务
|
||||||
async def async_call(self, service, service_data={}):
|
async def async_call(self, service, service_data={}):
|
||||||
|
|
|
@ -1,35 +1,35 @@
|
||||||
{
|
{
|
||||||
"title": "云音乐",
|
"title": "云音乐",
|
||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"abort": {
|
||||||
"single_instance_allowed": "仅允许单个配置"
|
"single_instance_allowed": "仅允许单个配置"
|
||||||
},
|
|
||||||
"step": {
|
|
||||||
"user": {
|
|
||||||
"title": "接口配置",
|
|
||||||
"description": "为防止你的账号密码泄露,建议自行部署API接口服务 \n免费部署文档:https://neteasecloudmusicapi.vercel.app \n实在是搞不来,也可以付费使用由我部署维护持续更新的接口服务😊",
|
|
||||||
"data": {
|
|
||||||
"url": "网易云音乐API"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"error": {
|
|
||||||
"login_failed": "登录失败"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"options": {
|
"step": {
|
||||||
"step": {
|
"user": {
|
||||||
"user": {
|
"title": "接口配置",
|
||||||
"title": "网易云音乐",
|
"description": "为防止你的账号密码泄露,建议自行部署API接口服务 \n免费部署文档:https://neteasecloudmusicapi.vercel.app \n实在是搞不来,也可以付费使用由我部署维护持续更新的接口服务😊",
|
||||||
"description": "登录后会将cookie保存在HomeAssistant存储目录之中",
|
"data": {
|
||||||
"data": {
|
"url": "网易云音乐API"
|
||||||
"username": "邮箱/手机号",
|
|
||||||
"password": "网易云音乐密码"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"error": {
|
|
||||||
"login_failed": "登录失败"
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"login_failed": "登录失败",
|
||||||
|
"api_failed": "接口地址不正确"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "配置",
|
||||||
|
"description": "关联的媒体播放器必须支持自定义音乐资源,可通过TTS插件自行测试是否可用",
|
||||||
|
"data": {
|
||||||
|
"media_player": "关联媒体播放器"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"login_failed": "登录失败"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue