├── .browserslistrc
├── .editorconfig
├── .eslintrc.js
├── .gitignore
├── .npmrc
├── .prettierrc
├── .yarnrc
├── LICENSE
├── README.md
├── babel.config.js
├── blueprints
└── homekit_music_remote.yaml
├── custom_components
└── ha_cloud_music
│ ├── README.md
│ ├── __init__.py
│ ├── api_config.py
│ ├── api_music.py
│ ├── api_tts.py
│ ├── api_view.py
│ ├── api_voice.py
│ ├── browse_media.py
│ ├── config_flow.py
│ ├── const.py
│ ├── dist
│ ├── css
│ │ ├── app.127813ec.css
│ │ ├── chunk-03b5c3a8.4028971c.css
│ │ ├── chunk-0a51bdcf.44557ed8.css
│ │ ├── chunk-71c58716.dd6e6a76.css
│ │ ├── chunk-94e4b18a.fc126ed6.css
│ │ ├── chunk-b2c00124.da0f97fd.css
│ │ ├── chunk-b39a67c6.4d3b1aac.css
│ │ ├── chunk-c98756a0.81258413.css
│ │ └── chunk-dd809a0c.8e6e487f.css
│ ├── favicon.ico
│ ├── img
│ │ ├── bg-1.cf743e29.jpg
│ │ ├── bg-2.a1183040.jpg
│ │ ├── player_cover.373e0739.png
│ │ └── warn.png
│ ├── index.html
│ ├── js
│ │ ├── app.608a90ca.js
│ │ ├── app.608a90ca.js.map
│ │ ├── chunk-03b5c3a8.0ab752ef.js
│ │ ├── chunk-03b5c3a8.0ab752ef.js.map
│ │ ├── chunk-0a51bdcf.9df0b307.js
│ │ ├── chunk-0a51bdcf.9df0b307.js.map
│ │ ├── chunk-71c58716.8a5fbf88.js
│ │ ├── chunk-71c58716.8a5fbf88.js.map
│ │ ├── chunk-94e4b18a.64861915.js
│ │ ├── chunk-94e4b18a.64861915.js.map
│ │ ├── chunk-b2c00124.392449b3.js
│ │ ├── chunk-b2c00124.392449b3.js.map
│ │ ├── chunk-b39a67c6.29b352e6.js
│ │ ├── chunk-b39a67c6.29b352e6.js.map
│ │ ├── chunk-c98756a0.e0d236a1.js
│ │ ├── chunk-c98756a0.e0d236a1.js.map
│ │ ├── chunk-dd809a0c.a42636c1.js
│ │ ├── chunk-dd809a0c.a42636c1.js.map
│ │ ├── chunk-vendors.a8ae6256.js
│ │ └── chunk-vendors.a8ae6256.js.map
│ └── prompt.html
│ ├── local
│ └── card
│ │ ├── MediaPlayer.js
│ │ ├── ha_cloud_music-card.js
│ │ ├── ha_cloud_music-fmlist.js
│ │ ├── ha_cloud_music-lovelist.js
│ │ ├── ha_cloud_music-panel.js
│ │ ├── ha_cloud_music-player.js
│ │ ├── ha_cloud_music-playlist.js
│ │ ├── ha_cloud_music-search-musiclist.js
│ │ ├── ha_cloud_music-search-playlist.js
│ │ ├── ha_cloud_music-search.js
│ │ ├── ha_cloud_music-setting.js
│ │ ├── ha_cloud_music-tabs.js
│ │ ├── ha_cloud_music-version.js
│ │ ├── ha_cloud_music-voice.js
│ │ ├── ha_cloud_music.js
│ │ └── test.html
│ ├── manifest.json
│ ├── media_player.py
│ ├── services.yaml
│ ├── shaonianzhentan.py
│ ├── source_mpd.py
│ ├── source_other.py
│ ├── source_vlc.py
│ ├── source_web.py
│ ├── source_windows.py
│ ├── test.py
│ ├── translations
│ └── en.json
│ └── util.py
├── hacs.json
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── favicon.ico
├── img
│ └── warn.png
├── index.html
└── prompt.html
├── src
├── App.vue
├── api
│ └── index.js
├── assets
│ ├── background
│ │ ├── bg-1.jpg
│ │ └── bg-2.jpg
│ └── img
│ │ ├── album_cover_player.png
│ │ ├── default.png
│ │ ├── player_cover.png
│ │ └── wave.gif
├── base
│ ├── mm-dialog
│ │ └── mm-dialog.vue
│ ├── mm-icon
│ │ └── mm-icon.vue
│ ├── mm-loading
│ │ └── mm-loading.vue
│ ├── mm-no-result
│ │ └── mm-no-result.vue
│ ├── mm-progress
│ │ └── mm-progress.vue
│ └── mm-toast
│ │ ├── index.js
│ │ └── mm-toast.vue
├── components
│ ├── lyric
│ │ └── lyric.vue
│ ├── mm-header
│ │ └── mm-header.vue
│ ├── music-btn
│ │ └── music-btn.vue
│ ├── music-list
│ │ └── music-list.vue
│ └── volume
│ │ └── volume.vue
├── config.js
├── main.js
├── pages
│ ├── comment
│ │ └── comment.vue
│ ├── details
│ │ └── details.vue
│ ├── historyList
│ │ └── historyList.vue
│ ├── mmPlayer.js
│ ├── music.vue
│ ├── playList
│ │ └── playList.vue
│ ├── search
│ │ └── search.vue
│ ├── topList
│ │ └── topList.vue
│ └── userList
│ │ └── userList.vue
├── router
│ └── index.js
├── store
│ ├── actions.js
│ ├── getters.js
│ ├── index.js
│ ├── mutation-types.js
│ ├── mutations.js
│ └── state.js
├── styles
│ ├── index.less
│ ├── mixin.less
│ ├── reset.less
│ └── var.less
└── utils
│ ├── audio.js
│ ├── axios.js
│ ├── hack.js
│ ├── mixin.js
│ ├── song.js
│ ├── storage.js
│ └── util.js
├── vue.config.js
└── yarn.lock
/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 2 versions
3 | not ie <= 8
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{js,jsx,ts,tsx,vue}]
2 | indent_style = space
3 | indent_size = 2
4 | trim_trailing_whitespace = true
5 | insert_final_newline = true
6 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true
5 | },
6 | extends: ['plugin:vue/recommended', '@vue/standard'],
7 | rules: {
8 | 'vue/max-attributes-per-line': [
9 | 2,
10 | {
11 | singleline: 10,
12 | multiline: {
13 | max: 1,
14 | allowFirstLine: false
15 | }
16 | }
17 | ],
18 | 'vue/singleline-html-element-content-newline': 'off',
19 | 'vue/multiline-html-element-content-newline': 'off',
20 | 'vue/name-property-casing': ['error', 'PascalCase'],
21 | 'vue/html-self-closing': [
22 | 'error',
23 | {
24 | html: {
25 | void: 'any',
26 | normal: 'never',
27 | component: 'always'
28 | },
29 | svg: 'always',
30 | math: 'always'
31 | }
32 | ],
33 | 'space-before-function-paren': [2, 'never'],
34 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
35 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
36 | 'no-sequences': 2,
37 | semi: 1
38 | },
39 | parserOptions: {
40 | parser: 'babel-eslint'
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 | __pycache__
5 | xmly.cookie
6 |
7 | # local env files
8 | .env.local
9 | .env.*.local
10 |
11 | # Log files
12 | npm-debug.log*
13 | yarn-debug.log*
14 | yarn-error.log*
15 |
16 | # Editor directories and files
17 | .idea
18 | .vscode
19 | *.suo
20 | *.ntvs*
21 | *.njsproj
22 | *.sln
23 | *.sw?
24 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | registry=https://registry.npm.taobao.org
2 | sass_binary_site=https://npm.taobao.org/mirrors/node-sass/
3 | phantomjs_cdnurl=http://cnpmjs.org/downloads
4 | electron_mirror=https://npm.taobao.org/mirrors/electron/
5 | sqlite3_binary_host_mirror=https://foxgis.oss-cn-shanghai.aliyuncs.com/
6 | profiler_binary_host_mirror=https://npm.taobao.org/mirrors/node-inspector/
7 | chromedriver_cdnurl=https://cdn.npm.taobao.org/dist/chromedriver
8 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true
4 | }
5 |
--------------------------------------------------------------------------------
/.yarnrc:
--------------------------------------------------------------------------------
1 | registry "https://registry.npm.taobao.org"
2 | sass_binary_site "https://npm.taobao.org/mirrors/node-sass/"
3 | phantomjs_cdnurl "http://cnpmjs.org/downloads"
4 | electron_mirror "https://npm.taobao.org/mirrors/electron/"
5 | sqlite3_binary_host_mirror "https://foxgis.oss-cn-shanghai.aliyuncs.com/"
6 | profiler_binary_host_mirror "https://npm.taobao.org/mirrors/node-inspector/"
7 | chromedriver_cdnurl "https://cdn.npm.taobao.org/dist/chromedriver"
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 maomao1996
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | '@vue/app'
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/blueprints/homekit_music_remote.yaml:
--------------------------------------------------------------------------------
1 | blueprint:
2 | name: 云音乐iOS遥控
3 | description: 云音乐插件iOS遥控器
4 | domain: automation
5 | source_url: https://github.com/shaonianzhentan/ha_cloud_music/blob/master/blueprints/homekit_music_remote.yaml
6 | input:
7 | select:
8 | name: 选择键
9 | description: 选择 select
10 | default: []
11 | selector:
12 | action: {}
13 | back:
14 | name: 返回键
15 | description: 返回 back
16 | default: []
17 | selector:
18 | action: {}
19 | information:
20 | name: 信息键
21 | description: 信息 information
22 | default: []
23 | selector:
24 | action: {}
25 | trigger:
26 | - platform: event
27 | event_type: homekit_tv_remote_key_pressed
28 | event_data:
29 | entity_id: media_player.yun_yin_le
30 | action:
31 | - variables:
32 | entity_id: media_player.yun_yin_le
33 | command: '{{ trigger.event.data.key_name }}'
34 | - choose:
35 | - conditions:
36 | - '{{ command == "play_pause" }}'
37 | sequence:
38 | - service: media_player.media_play_pause
39 | data:
40 | entity_id: '{{ entity_id }}'
41 | - conditions:
42 | - '{{ command == "arrow_left" }}'
43 | sequence:
44 | - service: media_player.media_previous_track
45 | data:
46 | entity_id: '{{ entity_id }}'
47 | - conditions:
48 | - '{{ command == "arrow_right" }}'
49 | sequence:
50 | - service: media_player.media_next_track
51 | data:
52 | entity_id: '{{ entity_id }}'
53 | - conditions:
54 | - '{{ command == "arrow_up" }}'
55 | sequence:
56 | - service: media_player.volume_up
57 | data:
58 | entity_id: '{{ entity_id }}'
59 | - conditions:
60 | - '{{ command == "arrow_down" }}'
61 | sequence:
62 | - service: media_player.volume_down
63 | data:
64 | entity_id: '{{ entity_id }}'
65 | - conditions:
66 | - '{{ command == "select" }}'
67 | sequence: !input 'select'
68 | - conditions:
69 | - '{{ command == "back" }}'
70 | sequence: !input 'back'
71 | - conditions:
72 | - '{{ command == "information" }}'
73 | sequence: !input 'information'
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/README.md:
--------------------------------------------------------------------------------
1 | ## load服务参数介绍
2 |
3 | ### 网易云音乐歌单
4 | - 歌单链接:https://music.163.com/#/playlist?id=25724904
5 | - 歌单id(25724904)
6 | - 歌单type(playlist)
7 |
8 | ### 网易云音乐电台
9 | - 电台链接:https://music.163.com/#/djradio?id=1008
10 | - 电台id(1008)
11 | - 电台type(djradio)
12 |
13 | ### 喜马拉雅专辑
14 | - 喜马拉雅专辑:https://www.ximalaya.com/qinggan/258244/
15 | - 专辑id(258244)
16 | - 专辑type(ximalaya)
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/__init__.py:
--------------------------------------------------------------------------------
1 | async def async_setup_entry(hass, config_entry):
2 | hass.async_create_task(hass.config_entries.async_forward_entry_setup(config_entry, "media_player"))
3 | return True
4 |
5 | from homeassistant.config_entries import ConfigEntry
6 | from homeassistant.core import HomeAssistant
7 | import homeassistant.helpers.config_validation as cv
8 |
9 | from .const import DOMAIN, PLATFORMS, NAME, ICON, DOMAIN, ROOT_PATH, VERSION
10 |
11 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
12 | hass.config_entries.async_setup_platforms(entry, PLATFORMS)
13 |
14 | entry.async_on_unload(entry.add_update_listener(update_listener))
15 | return True
16 |
17 | async def update_listener(hass, entry):
18 | """Handle options update."""
19 | config = entry.options
20 | mp = hass.data[DOMAIN]
21 | mp.api_tts.tts_before_message = config.get('tts_before_message', '')
22 | mp.api_tts.tts_after_message = config.get('tts_after_message', '')
23 | mp.api_music.find_api_url = config.get('find_api_url', '')
24 | mp.api_music.user = config.get('user', '')
25 | mp.api_music.password = config.get('password', '')
26 | def login_callback(uid):
27 | hass.components.frontend.async_remove_panel(DOMAIN)
28 | # 注册菜单栏
29 | hass.components.frontend.async_register_built_in_panel(
30 | "iframe", NAME, ICON, DOMAIN,
31 | { "url": ROOT_PATH + "/index.html?ver=" + VERSION + "&show_mode=default&uid=" + uid },
32 | require_admin=False
33 | )
34 | # 开始登录
35 | if mp.api_music.user != '' and mp.api_music.password != '':
36 | await mp.api_music.login(login_callback)
37 |
38 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
39 | return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/browse_media.py:
--------------------------------------------------------------------------------
1 | """Support for media browsing."""
2 | import logging, os
3 | from homeassistant.helpers.network import get_url
4 | from homeassistant.components.media_player import BrowseError, BrowseMedia
5 | from homeassistant.components.media_player.const import (
6 | MEDIA_CLASS_ALBUM,
7 | MEDIA_CLASS_ARTIST,
8 | MEDIA_CLASS_CHANNEL,
9 | MEDIA_CLASS_DIRECTORY,
10 | MEDIA_CLASS_EPISODE,
11 | MEDIA_CLASS_MOVIE,
12 | MEDIA_CLASS_MUSIC,
13 | MEDIA_CLASS_PLAYLIST,
14 | MEDIA_CLASS_SEASON,
15 | MEDIA_CLASS_TRACK,
16 | MEDIA_CLASS_TV_SHOW,
17 | MEDIA_TYPE_ALBUM,
18 | MEDIA_TYPE_ARTIST,
19 | MEDIA_TYPE_CHANNEL,
20 | MEDIA_TYPE_EPISODE,
21 | MEDIA_TYPE_MOVIE,
22 | MEDIA_TYPE_PLAYLIST,
23 | MEDIA_TYPE_SEASON,
24 | MEDIA_TYPE_TRACK,
25 | MEDIA_TYPE_TVSHOW,
26 | )
27 |
28 | PLAYABLE_MEDIA_TYPES = [
29 | MEDIA_TYPE_ALBUM,
30 | MEDIA_TYPE_ARTIST,
31 | MEDIA_TYPE_TRACK,
32 | ]
33 |
34 | CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS = {
35 | MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM,
36 | MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST,
37 | MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST,
38 | MEDIA_TYPE_SEASON: MEDIA_CLASS_SEASON,
39 | MEDIA_TYPE_TVSHOW: MEDIA_CLASS_TV_SHOW,
40 | }
41 |
42 | CHILD_TYPE_MEDIA_CLASS = {
43 | MEDIA_TYPE_SEASON: MEDIA_CLASS_SEASON,
44 | MEDIA_TYPE_ALBUM: MEDIA_CLASS_ALBUM,
45 | MEDIA_TYPE_ARTIST: MEDIA_CLASS_ARTIST,
46 | MEDIA_TYPE_MOVIE: MEDIA_CLASS_MOVIE,
47 | MEDIA_TYPE_PLAYLIST: MEDIA_CLASS_PLAYLIST,
48 | MEDIA_TYPE_TRACK: MEDIA_CLASS_TRACK,
49 | MEDIA_TYPE_TVSHOW: MEDIA_CLASS_TV_SHOW,
50 | MEDIA_TYPE_CHANNEL: MEDIA_CLASS_CHANNEL,
51 | MEDIA_TYPE_EPISODE: MEDIA_CLASS_EPISODE,
52 | }
53 |
54 | _LOGGER = logging.getLogger(__name__)
55 |
56 |
57 | class UnknownMediaType(BrowseError):
58 | """Unknown media type."""
59 |
60 |
61 | async def build_item_response(media_library, payload):
62 | """Create response payload for the provided media query."""
63 | # print(payload)
64 | search_id = payload["search_id"]
65 | search_type = payload["search_type"]
66 | hass = media_library._hass
67 | thumbnail = None
68 | title = None
69 | media = None
70 | media_class = MEDIA_CLASS_DIRECTORY
71 | can_play = False
72 | can_expand = True
73 | children = []
74 | base_url = get_url(hass)
75 | is_library = 'library_' in search_type
76 |
77 | properties = ["thumbnail"]
78 | if is_library:
79 | # 读取配置目录
80 | path = hass.config.path("media/ha_cloud_music")
81 | # 获取所有文件
82 | music_list = media_library.api_music.get_local_media_list(search_type)
83 | for item in music_list:
84 | children.append(item_payload({
85 | "label": item['name'], "type": 'music', "songid": item['url']
86 | }, media_library))
87 |
88 | title = search_type.replace('library_', '')
89 | media_class = MEDIA_CLASS_MUSIC
90 | can_play = True
91 | can_expand = False
92 |
93 | response = BrowseMedia(
94 | media_class=media_class,
95 | media_content_id=search_id,
96 | media_content_type=search_type,
97 | title=title,
98 | can_play=can_play,
99 | can_expand=can_expand,
100 | children=children,
101 | thumbnail=thumbnail,
102 | )
103 |
104 | if is_library:
105 | response.children_media_class = MEDIA_CLASS_MUSIC
106 | else:
107 | response.calculate_children_class()
108 |
109 | return response
110 |
111 |
112 | def item_payload(item, media_library):
113 | # print(item)
114 | title = item["label"]
115 | media_class = None
116 | media_content_type = item["type"]
117 |
118 | if "songid" in item:
119 | # 音乐
120 | media_class = MEDIA_CLASS_MUSIC
121 | media_content_id = f"{item['songid']}"
122 | can_play = True
123 | can_expand = False
124 | else:
125 | # 目录
126 | media_class = MEDIA_CLASS_DIRECTORY
127 | media_content_id = ""
128 | can_play = False
129 | can_expand = True
130 |
131 | return BrowseMedia(
132 | title=title,
133 | media_class=media_class,
134 | media_content_type=media_content_type,
135 | media_content_id=media_content_id,
136 | can_play=can_play,
137 | can_expand=can_expand
138 | )
139 |
140 |
141 | def library_payload(media_library):
142 | """
143 | 创建音乐库
144 | """
145 | library_info = BrowseMedia(
146 | media_class=MEDIA_CLASS_DIRECTORY,
147 | media_content_id="library",
148 | media_content_type="library",
149 | title="Media Library",
150 | can_play=False,
151 | can_expand=True,
152 | children=[],
153 | )
154 | # 默认列表
155 | library_info.children.append(
156 | item_payload(
157 | {"label": "默认列表", "type": "library_music"},
158 | media_library,
159 | )
160 | )
161 | # 读取文件夹
162 | path = media_library._hass.config.path("media/ha_cloud_music")
163 | for filename in os.listdir(path):
164 | if os.path.isdir(os.path.join(path, filename)):
165 | library_info.children.append(
166 | item_payload(
167 | {"label": filename, "type": f"library_{filename}"},
168 | media_library,
169 | )
170 | )
171 | return library_info
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/config_flow.py:
--------------------------------------------------------------------------------
1 | """Config flow for Hello World integration."""
2 | import logging
3 |
4 | import voluptuous as vol
5 |
6 | from homeassistant import config_entries
7 | import homeassistant.helpers.config_validation as cv
8 | from homeassistant.core import callback
9 | from homeassistant.config_entries import ConfigFlow, OptionsFlow, ConfigEntry
10 |
11 | from .const import DOMAIN # pylint:disable=unused-import
12 |
13 | _LOGGER = logging.getLogger(__name__)
14 |
15 | class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
16 |
17 | VERSION = 1
18 | CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
19 |
20 | async def async_step_user(self, user_input=None):
21 | errors = {}
22 | if DOMAIN in self.hass.data:
23 | return self.async_abort(reason="single_instance_allowed")
24 |
25 | # 如果输入内容不为空,则进行验证
26 | if user_input is not None:
27 | user_input['api_url'] = user_input['api_url'].strip('/')
28 | return self.async_create_entry(title=DOMAIN, data=user_input)
29 |
30 | # 显示表单
31 | DATA_SCHEMA = vol.Schema({
32 | vol.Required("api_url", default="https://netease-cloud-music-api-7k8q.vercel.app"): str,
33 | vol.Optional("mpd_host"): str,
34 | vol.Optional("is_voice", default=True): bool,
35 | })
36 | return self.async_show_form(
37 | step_id="user", data_schema=DATA_SCHEMA, errors=errors
38 | )
39 |
40 | @staticmethod
41 | @callback
42 | def async_get_options_flow(entry: ConfigEntry):
43 | return OptionsFlowHandler(entry)
44 |
45 |
46 | class OptionsFlowHandler(OptionsFlow):
47 | def __init__(self, config_entry: ConfigEntry):
48 | self.config_entry = config_entry
49 |
50 | async def async_step_init(self, user_input=None):
51 | return await self.async_step_user(user_input)
52 |
53 | async def async_step_user(self, user_input=None):
54 | errors = {}
55 | if user_input is None:
56 | options = self.config_entry.options
57 | errors = {}
58 | DATA_SCHEMA = vol.Schema({
59 | vol.Optional("find_api_url", default=options.get('find_api_url', '')): str,
60 | vol.Optional("user", default=options.get('user', '')): str,
61 | vol.Optional("password", default=options.get('password', '')): str,
62 | vol.Optional("tts_before_message", default=options.get('tts_before_message', '')): str,
63 | vol.Optional("tts_after_message", default=options.get('tts_after_message', '')): str,
64 | vol.Optional("is_notify", default=options.get('is_notify', True)): bool,
65 | vol.Optional("tts_mode", default=options.get('tts_mode', 4)): int,
66 | })
67 | return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA, errors=errors)
68 | # 选项更新
69 | user_input['find_api_url'] = user_input['find_api_url'].strip('/')
70 | return self.async_create_entry(title=DOMAIN, data=user_input)
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/const.py:
--------------------------------------------------------------------------------
1 | NAME = '云音乐'
2 | ICON = 'mdi:music'
3 | DOMAIN = 'ha_cloud_music'
4 | VERSION = '4.7.7'
5 | DOMAIN_API = '/' + DOMAIN + '-api'
6 | WEB_PATH = '/' + DOMAIN + '-web'
7 | ROOT_PATH = '/' + DOMAIN + '-local/' + VERSION
8 | PLATFORMS = ["media_player"]
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/dist/css/chunk-0a51bdcf.44557ed8.css:
--------------------------------------------------------------------------------
1 | .mm-no-result{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;width:100%;height:100%}.mm-no-result-text{margin-top:30px;font-size:14px;color:hsla(0,0%,100%,.6)}.list-header[data-v-00282ac4]{border-bottom:1px solid hsla(0,0%,100%,.8);color:#fff}.list-header .list-name[data-v-00282ac4]{padding-left:40px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.list-content[data-v-00282ac4]{width:100%;height:calc(100% - 60px);overflow-x:hidden;overflow-y:auto;-webkit-overflow-scrolling:touch}.list-no[data-v-00282ac4]{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;height:100%;color:hsla(0,0%,100%,.6)}.list-item[data-v-00282ac4],.list-no[data-v-00282ac4]{display:-webkit-box;display:-ms-flexbox;display:flex;width:100%}.list-item[data-v-00282ac4]{height:50px;border-bottom:1px solid hsla(0,0%,100%,.1);line-height:50px;overflow:hidden}.list-item.list-item-no[data-v-00282ac4]{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.list-item.on[data-v-00282ac4]{color:#fff}.list-item.on .list-num[data-v-00282ac4]{font-size:0;background:url(data:image/gif;base64,R0lGODlhCgAKAIABAP///////yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJBAABACwAAAAACgAKAAACEIyPqcsM4GB4iM4nLc5c8wIAIfkECQQAAQAsAAAAAAoACgAAAhGMj6mrAOxMPNPVFyy+V1rvFQAh+QQJBAABACwAAAAACgAKAAACE4yPqQew0UxkDrY6L96Zb3mBYAEAIfkECQQAAQAsAAAAAAoACgAAAhSMjwiQt/YcCyvaCmfV/GZPYWFWAAAh+QQJBAABACwAAAAACgAKAAACFIyPeQDIxl6QsU1bIY5a0/15m1YAACH5BAkEAAEALAAAAAAKAAoAAAIUjI+ZAMpsYHhtVhrvy1n6bXVaVgAAIfkECQQAAQAsAAAAAAoACgAAAhOMjwCYy6aeioFOZyfEuvK8gV4BACH5BAkEAAEALAAAAAAKAAoAAAISjI+ZAGrsAlysSVuxnry+rX0FACH5BAkEAAEALAAAAAAKAAoAAAIRjI+pawDs4EsuBlsnrk3z6xUAIfkECQQAAQAsAAAAAAoACgAAAhCMj6nLCMBehOnJcC3N1vACACH5BAkEAAEALAAAAAAKAAoAAAIPjI+py50AGJTxwTrDxbwAACH5BAkEAAEALAAAAAAKAAoAAAIPjI+py+0JAIuGHoujDLkAACH5BAkEAAEALAAAAAAKAAoAAAIQjI+py+0NAIqGhmgzrvKCAgAh+QQJBAABACwAAAAACgAKAAACD4yPqcvdABg081R4Y8A6FwA7LyogIHx4R3YwMHxkNDJiOTgyYzhjM2YwYWExNzcxNDk4OTU2ZjY3ODc0MSAqLw==) no-repeat 50%}.list-item:hover .list-name[data-v-00282ac4]{padding-right:80px}.list-item:hover .list-name .list-menu[data-v-00282ac4]{display:block}.list-item:not([class*=list-header]):hover .list-name[data-v-00282ac4]{padding-right:80px}.list-item:not([class*=list-header]):hover .list-name .list-menu[data-v-00282ac4]{display:block}.list-item:not([class*=list-header]):hover .list-time[data-v-00282ac4]{font-size:0}.list-item:not([class*=list-header]):hover .list-time .list-menu-icon-del[data-v-00282ac4]{display:block}.list-item .list-num[data-v-00282ac4]{display:block;width:30px;margin-right:10px;text-align:center}.list-item .list-name[data-v-00282ac4]{position:relative;-webkit-box-flex:1;-ms-flex:1;flex:1;-webkit-box-sizing:border-box;box-sizing:border-box}.list-item .list-name>span[data-v-00282ac4]{text-overflow:ellipsis;overflow:hidden;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical}.list-item .list-name small[data-v-00282ac4]{margin-left:5px;font-size:12px;color:hsla(0,0%,100%,.5)}.list-item .list-name .list-menu[data-v-00282ac4]{display:none;position:absolute;top:50%;right:10px;height:40px;font-size:0;-webkit-transform:translateY(-50%);transform:translateY(-50%)}.list-item .list-album[data-v-00282ac4],.list-item .list-artist[data-v-00282ac4]{display:block;width:300px;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}@media (max-width:1440px){.list-item .list-album[data-v-00282ac4],.list-item .list-artist[data-v-00282ac4]{width:200px}}@media (max-width:1200px){.list-item .list-album[data-v-00282ac4],.list-item .list-artist[data-v-00282ac4]{width:150px}}.list-item .list-time[data-v-00282ac4]{display:block;width:60px;position:relative}.list-item .list-time .list-menu-icon-del[data-v-00282ac4]{display:none;position:absolute;top:50%;left:0;-webkit-transform:translateY(-50%);transform:translateY(-50%)}@media (max-width:960px){.list-item .list-name[data-v-00282ac4]{padding-right:70px}}@media (max-width:768px){.list-item .list-name .list-menu[data-v-00282ac4]{display:block}.list-item .list-album[data-v-00282ac4],.list-item .list-artist[data-v-00282ac4]{width:20%}}@media (max-width:640px){.list-item .list-artist[data-v-00282ac4]{width:80px}.list-item .list-album[data-v-00282ac4],.list-item .list-time[data-v-00282ac4]{display:none}}.search[data-v-a6b7aa78]{position:relative;width:100%;height:100%}.search .search-head[data-v-a6b7aa78]{display:-webkit-box;display:-ms-flexbox;display:flex;height:40px;padding:10px 15px;overflow:hidden;background:rgba(0,0,0,.2)}.search .search-head span[data-v-a6b7aa78]{line-height:40px;margin-right:15px;cursor:pointer}.search .search-head span[data-v-a6b7aa78]:hover{color:#fff}@media (max-width:640px){.search .search-head span[data-v-a6b7aa78]{display:none}}.search .search-head .search-input[data-v-a6b7aa78]{-webkit-box-flex:1;-ms-flex:1;flex:1;height:40px;-webkit-box-sizing:border-box;box-sizing:border-box;padding:0 15px;border:1px solid hsla(0,0%,100%,.6);outline:0;background:transparent;color:#fff;font-size:14px;-webkit-box-shadow:0 0 1px 0 #fff inset;box-shadow:inset 0 0 1px 0 #fff}.search .search-head .search-input[data-v-a6b7aa78]::-webkit-input-placeholder{color:hsla(0,0%,100%,.6)}.search .search-head .search-input[data-v-a6b7aa78]::-moz-placeholder{color:hsla(0,0%,100%,.6)}.search .search-head .search-input[data-v-a6b7aa78]:-ms-input-placeholder{color:hsla(0,0%,100%,.6)}.search .search-head .search-input[data-v-a6b7aa78]::-ms-input-placeholder{color:hsla(0,0%,100%,.6)}.search .search-head .search-input[data-v-a6b7aa78]::placeholder{color:hsla(0,0%,100%,.6)}.search .musicList[data-v-a6b7aa78]{width:100%;height:calc(100% - 50px)}
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/dist/css/chunk-71c58716.dd6e6a76.css:
--------------------------------------------------------------------------------
1 | .mm-no-result{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;width:100%;height:100%}.mm-no-result-text{margin-top:30px;font-size:14px;color:hsla(0,0%,100%,.6)}.userList[data-v-2e82723b]{position:relative;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;-webkit-overflow-scrolling:touch}.userList-head[data-v-2e82723b]{height:100px}.userList .list-item[data-v-2e82723b]{float:left;width:14.28571%}.userList .list-item .userList-item[data-v-2e82723b]{width:130px;text-align:center;cursor:pointer;margin:0 auto 20px}.userList .list-item .userList-item[data-v-2e82723b]:hover{color:#fff}.userList .list-item .userList-item .name[data-v-2e82723b]{height:30px;line-height:30px;font-size:14px;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}@media (max-width:1100px){.userList .list-item .userList-item[data-v-2e82723b]{width:80%}}@media (max-width:1500px){.userList .list-item[data-v-2e82723b]{width:16.66667%}}@media (max-width:960px),(max-width:1400px){.userList .list-item[data-v-2e82723b]{width:20%}}@media (max-width:768px),(max-width:1280px){.userList .list-item[data-v-2e82723b]{width:25%}}@media (max-width:540px){.userList .list-item[data-v-2e82723b]{width:33.33333%}}
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/dist/css/chunk-94e4b18a.fc126ed6.css:
--------------------------------------------------------------------------------
1 | .mm-no-result{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;width:100%;height:100%}.mm-no-result-text{margin-top:30px;font-size:14px;color:hsla(0,0%,100%,.6)}.list-header[data-v-00282ac4]{border-bottom:1px solid hsla(0,0%,100%,.8);color:#fff}.list-header .list-name[data-v-00282ac4]{padding-left:40px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.list-content[data-v-00282ac4]{width:100%;height:calc(100% - 60px);overflow-x:hidden;overflow-y:auto;-webkit-overflow-scrolling:touch}.list-no[data-v-00282ac4]{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;height:100%;color:hsla(0,0%,100%,.6)}.list-item[data-v-00282ac4],.list-no[data-v-00282ac4]{display:-webkit-box;display:-ms-flexbox;display:flex;width:100%}.list-item[data-v-00282ac4]{height:50px;border-bottom:1px solid hsla(0,0%,100%,.1);line-height:50px;overflow:hidden}.list-item.list-item-no[data-v-00282ac4]{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.list-item.on[data-v-00282ac4]{color:#fff}.list-item.on .list-num[data-v-00282ac4]{font-size:0;background:url(data:image/gif;base64,R0lGODlhCgAKAIABAP///////yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJBAABACwAAAAACgAKAAACEIyPqcsM4GB4iM4nLc5c8wIAIfkECQQAAQAsAAAAAAoACgAAAhGMj6mrAOxMPNPVFyy+V1rvFQAh+QQJBAABACwAAAAACgAKAAACE4yPqQew0UxkDrY6L96Zb3mBYAEAIfkECQQAAQAsAAAAAAoACgAAAhSMjwiQt/YcCyvaCmfV/GZPYWFWAAAh+QQJBAABACwAAAAACgAKAAACFIyPeQDIxl6QsU1bIY5a0/15m1YAACH5BAkEAAEALAAAAAAKAAoAAAIUjI+ZAMpsYHhtVhrvy1n6bXVaVgAAIfkECQQAAQAsAAAAAAoACgAAAhOMjwCYy6aeioFOZyfEuvK8gV4BACH5BAkEAAEALAAAAAAKAAoAAAISjI+ZAGrsAlysSVuxnry+rX0FACH5BAkEAAEALAAAAAAKAAoAAAIRjI+pawDs4EsuBlsnrk3z6xUAIfkECQQAAQAsAAAAAAoACgAAAhCMj6nLCMBehOnJcC3N1vACACH5BAkEAAEALAAAAAAKAAoAAAIPjI+py50AGJTxwTrDxbwAACH5BAkEAAEALAAAAAAKAAoAAAIPjI+py+0JAIuGHoujDLkAACH5BAkEAAEALAAAAAAKAAoAAAIQjI+py+0NAIqGhmgzrvKCAgAh+QQJBAABACwAAAAACgAKAAACD4yPqcvdABg081R4Y8A6FwA7LyogIHx4R3YwMHxkNDJiOTgyYzhjM2YwYWExNzcxNDk4OTU2ZjY3ODc0MSAqLw==) no-repeat 50%}.list-item:hover .list-name[data-v-00282ac4]{padding-right:80px}.list-item:hover .list-name .list-menu[data-v-00282ac4]{display:block}.list-item:not([class*=list-header]):hover .list-name[data-v-00282ac4]{padding-right:80px}.list-item:not([class*=list-header]):hover .list-name .list-menu[data-v-00282ac4]{display:block}.list-item:not([class*=list-header]):hover .list-time[data-v-00282ac4]{font-size:0}.list-item:not([class*=list-header]):hover .list-time .list-menu-icon-del[data-v-00282ac4]{display:block}.list-item .list-num[data-v-00282ac4]{display:block;width:30px;margin-right:10px;text-align:center}.list-item .list-name[data-v-00282ac4]{position:relative;-webkit-box-flex:1;-ms-flex:1;flex:1;-webkit-box-sizing:border-box;box-sizing:border-box}.list-item .list-name>span[data-v-00282ac4]{text-overflow:ellipsis;overflow:hidden;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical}.list-item .list-name small[data-v-00282ac4]{margin-left:5px;font-size:12px;color:hsla(0,0%,100%,.5)}.list-item .list-name .list-menu[data-v-00282ac4]{display:none;position:absolute;top:50%;right:10px;height:40px;font-size:0;-webkit-transform:translateY(-50%);transform:translateY(-50%)}.list-item .list-album[data-v-00282ac4],.list-item .list-artist[data-v-00282ac4]{display:block;width:300px;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}@media (max-width:1440px){.list-item .list-album[data-v-00282ac4],.list-item .list-artist[data-v-00282ac4]{width:200px}}@media (max-width:1200px){.list-item .list-album[data-v-00282ac4],.list-item .list-artist[data-v-00282ac4]{width:150px}}.list-item .list-time[data-v-00282ac4]{display:block;width:60px;position:relative}.list-item .list-time .list-menu-icon-del[data-v-00282ac4]{display:none;position:absolute;top:50%;left:0;-webkit-transform:translateY(-50%);transform:translateY(-50%)}@media (max-width:960px){.list-item .list-name[data-v-00282ac4]{padding-right:70px}}@media (max-width:768px){.list-item .list-name .list-menu[data-v-00282ac4]{display:block}.list-item .list-album[data-v-00282ac4],.list-item .list-artist[data-v-00282ac4]{width:20%}}@media (max-width:640px){.list-item .list-artist[data-v-00282ac4]{width:80px}.list-item .list-album[data-v-00282ac4],.list-item .list-time[data-v-00282ac4]{display:none}}.playList{position:relative}.playList,.playList .musicList{width:100%;height:100%}.playList .musicList .list-btn{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;height:50px}.playList .musicList .list-btn span{padding:5px 20px;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.playList .musicList .list-btn span:hover{color:#fff}
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/dist/css/chunk-b2c00124.da0f97fd.css:
--------------------------------------------------------------------------------
1 | .comment[data-v-197b1e4b]{position:relative;-webkit-transform:translateZ(0);transform:translateZ(0);width:100%;height:100%;overflow:hidden}.comment .comment-list[data-v-197b1e4b]{height:100%;padding:0 10px;overflow-x:hidden;overflow-y:auto;-webkit-overflow-scrolling:touch}.comment .comment-list .comment-title[data-v-197b1e4b]{height:34px;line-height:34px;padding:10px 0;color:#fff;border-bottom:1px solid hsla(0,0%,100%,.8)}.comment .comment-list .comment-item[data-v-197b1e4b]{position:relative;padding:15px 0 15px 55px;border-bottom:1px solid hsla(0,0%,100%,.1)}.comment .comment-list .comment-item .comment-item-pic[data-v-197b1e4b]{display:block;position:absolute;left:0;top:20px;width:38px;height:38px;border-radius:50%;overflow:hidden}.comment .comment-list .comment-item .comment-item-title[data-v-197b1e4b]{height:20px;margin-bottom:6px;font-weight:400;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;color:#fff}.comment .comment-list .comment-item .comment-item-disc[data-v-197b1e4b]{overflow:hidden;word-break:break-all;word-wrap:break-word;line-height:25px;text-align:justify;color:hsla(0,0%,100%,.6)}.comment .comment-list .comment-item .comment-item-disc img[data-v-197b1e4b]{position:relative;vertical-align:middle;top:-2px}.comment .comment-list .comment-item .comment-item-replied[data-v-197b1e4b]{padding:8px 19px;margin-top:10px;line-height:20px;border:1px solid hsla(0,0%,100%,.3)}.comment .comment-list .comment-item .comment-item-replied a[data-v-197b1e4b]{color:#fff}.comment .comment-list .comment-item .comment-item-opt[data-v-197b1e4b]{margin-top:10px;line-height:25px;text-align:right;overflow:hidden}.comment .comment-list .comment-item .comment-item-opt .comment-opt-date[data-v-197b1e4b]{float:left;line-height:28px}.comment .comment-list .comment-item .comment-item-opt .comment-opt-liked[data-v-197b1e4b]{display:inline-block;height:20px;line-height:20px}
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/dist/css/chunk-b39a67c6.4d3b1aac.css:
--------------------------------------------------------------------------------
1 | .topList[data-v-4cb254da]{position:relative;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;-webkit-overflow-scrolling:touch}.topList-head[data-v-4cb254da]{width:100%;height:34px;line-height:34px;padding:20px 0;font-size:18px;color:#fff}.topList-content[data-v-4cb254da]{overflow:hidden}.topList .list-item[data-v-4cb254da]{float:left;width:14.28571%}.topList .list-item .topList-item[data-v-4cb254da]{width:130px;text-align:center;cursor:pointer;margin:0 auto 20px}.topList .list-item .topList-item[data-v-4cb254da]:hover{color:#fff}.topList .list-item .topList-item .name[data-v-4cb254da]{height:30px;line-height:30px;font-size:14px;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}@media (max-width:1100px){.topList .list-item .topList-item[data-v-4cb254da]{width:80%}}@media (max-width:1500px){.topList .list-item[data-v-4cb254da]{width:16.66667%}}@media (max-width:960px),(max-width:1400px){.topList .list-item[data-v-4cb254da]{width:20%}}@media (max-width:768px),(max-width:1280px){.topList .list-item[data-v-4cb254da]{width:25%}}@media (max-width:540px){.topList .list-item[data-v-4cb254da]{width:33.33333%}}.topList .list-item .topList-img[data-v-4cb254da]{position:relative;padding-top:100%;width:100%;height:0}.topList .list-item .topList-img .cover-img[data-v-4cb254da]{position:absolute;top:0;left:0}
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/dist/css/chunk-c98756a0.81258413.css:
--------------------------------------------------------------------------------
1 | .mm-no-result{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;width:100%;height:100%}.mm-no-result-text{margin-top:30px;font-size:14px;color:hsla(0,0%,100%,.6)}.list-header[data-v-00282ac4]{border-bottom:1px solid hsla(0,0%,100%,.8);color:#fff}.list-header .list-name[data-v-00282ac4]{padding-left:40px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.list-content[data-v-00282ac4]{width:100%;height:calc(100% - 60px);overflow-x:hidden;overflow-y:auto;-webkit-overflow-scrolling:touch}.list-no[data-v-00282ac4]{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;height:100%;color:hsla(0,0%,100%,.6)}.list-item[data-v-00282ac4],.list-no[data-v-00282ac4]{display:-webkit-box;display:-ms-flexbox;display:flex;width:100%}.list-item[data-v-00282ac4]{height:50px;border-bottom:1px solid hsla(0,0%,100%,.1);line-height:50px;overflow:hidden}.list-item.list-item-no[data-v-00282ac4]{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.list-item.on[data-v-00282ac4]{color:#fff}.list-item.on .list-num[data-v-00282ac4]{font-size:0;background:url(data:image/gif;base64,R0lGODlhCgAKAIABAP///////yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJBAABACwAAAAACgAKAAACEIyPqcsM4GB4iM4nLc5c8wIAIfkECQQAAQAsAAAAAAoACgAAAhGMj6mrAOxMPNPVFyy+V1rvFQAh+QQJBAABACwAAAAACgAKAAACE4yPqQew0UxkDrY6L96Zb3mBYAEAIfkECQQAAQAsAAAAAAoACgAAAhSMjwiQt/YcCyvaCmfV/GZPYWFWAAAh+QQJBAABACwAAAAACgAKAAACFIyPeQDIxl6QsU1bIY5a0/15m1YAACH5BAkEAAEALAAAAAAKAAoAAAIUjI+ZAMpsYHhtVhrvy1n6bXVaVgAAIfkECQQAAQAsAAAAAAoACgAAAhOMjwCYy6aeioFOZyfEuvK8gV4BACH5BAkEAAEALAAAAAAKAAoAAAISjI+ZAGrsAlysSVuxnry+rX0FACH5BAkEAAEALAAAAAAKAAoAAAIRjI+pawDs4EsuBlsnrk3z6xUAIfkECQQAAQAsAAAAAAoACgAAAhCMj6nLCMBehOnJcC3N1vACACH5BAkEAAEALAAAAAAKAAoAAAIPjI+py50AGJTxwTrDxbwAACH5BAkEAAEALAAAAAAKAAoAAAIPjI+py+0JAIuGHoujDLkAACH5BAkEAAEALAAAAAAKAAoAAAIQjI+py+0NAIqGhmgzrvKCAgAh+QQJBAABACwAAAAACgAKAAACD4yPqcvdABg081R4Y8A6FwA7LyogIHx4R3YwMHxkNDJiOTgyYzhjM2YwYWExNzcxNDk4OTU2ZjY3ODc0MSAqLw==) no-repeat 50%}.list-item:hover .list-name[data-v-00282ac4]{padding-right:80px}.list-item:hover .list-name .list-menu[data-v-00282ac4]{display:block}.list-item:not([class*=list-header]):hover .list-name[data-v-00282ac4]{padding-right:80px}.list-item:not([class*=list-header]):hover .list-name .list-menu[data-v-00282ac4]{display:block}.list-item:not([class*=list-header]):hover .list-time[data-v-00282ac4]{font-size:0}.list-item:not([class*=list-header]):hover .list-time .list-menu-icon-del[data-v-00282ac4]{display:block}.list-item .list-num[data-v-00282ac4]{display:block;width:30px;margin-right:10px;text-align:center}.list-item .list-name[data-v-00282ac4]{position:relative;-webkit-box-flex:1;-ms-flex:1;flex:1;-webkit-box-sizing:border-box;box-sizing:border-box}.list-item .list-name>span[data-v-00282ac4]{text-overflow:ellipsis;overflow:hidden;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical}.list-item .list-name small[data-v-00282ac4]{margin-left:5px;font-size:12px;color:hsla(0,0%,100%,.5)}.list-item .list-name .list-menu[data-v-00282ac4]{display:none;position:absolute;top:50%;right:10px;height:40px;font-size:0;-webkit-transform:translateY(-50%);transform:translateY(-50%)}.list-item .list-album[data-v-00282ac4],.list-item .list-artist[data-v-00282ac4]{display:block;width:300px;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}@media (max-width:1440px){.list-item .list-album[data-v-00282ac4],.list-item .list-artist[data-v-00282ac4]{width:200px}}@media (max-width:1200px){.list-item .list-album[data-v-00282ac4],.list-item .list-artist[data-v-00282ac4]{width:150px}}.list-item .list-time[data-v-00282ac4]{display:block;width:60px;position:relative}.list-item .list-time .list-menu-icon-del[data-v-00282ac4]{display:none;position:absolute;top:50%;left:0;-webkit-transform:translateY(-50%);transform:translateY(-50%)}@media (max-width:960px){.list-item .list-name[data-v-00282ac4]{padding-right:70px}}@media (max-width:768px){.list-item .list-name .list-menu[data-v-00282ac4]{display:block}.list-item .list-album[data-v-00282ac4],.list-item .list-artist[data-v-00282ac4]{width:20%}}@media (max-width:640px){.list-item .list-artist[data-v-00282ac4]{width:80px}.list-item .list-album[data-v-00282ac4],.list-item .list-time[data-v-00282ac4]{display:none}}.historyList .musicList[data-v-2d87d009],.historyList[data-v-2d87d009]{width:100%;height:100%}.historyList .musicList .list-btn[data-v-2d87d009]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;height:50px}.historyList .musicList .list-btn span[data-v-2d87d009]{padding:5px 20px;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.historyList .musicList .list-btn span[data-v-2d87d009]:hover{color:#fff}
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/dist/css/chunk-dd809a0c.8e6e487f.css:
--------------------------------------------------------------------------------
1 | .mm-no-result{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;width:100%;height:100%}.mm-no-result-text{margin-top:30px;font-size:14px;color:hsla(0,0%,100%,.6)}.list-header[data-v-00282ac4]{border-bottom:1px solid hsla(0,0%,100%,.8);color:#fff}.list-header .list-name[data-v-00282ac4]{padding-left:40px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.list-content[data-v-00282ac4]{width:100%;height:calc(100% - 60px);overflow-x:hidden;overflow-y:auto;-webkit-overflow-scrolling:touch}.list-no[data-v-00282ac4]{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;height:100%;color:hsla(0,0%,100%,.6)}.list-item[data-v-00282ac4],.list-no[data-v-00282ac4]{display:-webkit-box;display:-ms-flexbox;display:flex;width:100%}.list-item[data-v-00282ac4]{height:50px;border-bottom:1px solid hsla(0,0%,100%,.1);line-height:50px;overflow:hidden}.list-item.list-item-no[data-v-00282ac4]{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.list-item.on[data-v-00282ac4]{color:#fff}.list-item.on .list-num[data-v-00282ac4]{font-size:0;background:url(data:image/gif;base64,R0lGODlhCgAKAIABAP///////yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJBAABACwAAAAACgAKAAACEIyPqcsM4GB4iM4nLc5c8wIAIfkECQQAAQAsAAAAAAoACgAAAhGMj6mrAOxMPNPVFyy+V1rvFQAh+QQJBAABACwAAAAACgAKAAACE4yPqQew0UxkDrY6L96Zb3mBYAEAIfkECQQAAQAsAAAAAAoACgAAAhSMjwiQt/YcCyvaCmfV/GZPYWFWAAAh+QQJBAABACwAAAAACgAKAAACFIyPeQDIxl6QsU1bIY5a0/15m1YAACH5BAkEAAEALAAAAAAKAAoAAAIUjI+ZAMpsYHhtVhrvy1n6bXVaVgAAIfkECQQAAQAsAAAAAAoACgAAAhOMjwCYy6aeioFOZyfEuvK8gV4BACH5BAkEAAEALAAAAAAKAAoAAAISjI+ZAGrsAlysSVuxnry+rX0FACH5BAkEAAEALAAAAAAKAAoAAAIRjI+pawDs4EsuBlsnrk3z6xUAIfkECQQAAQAsAAAAAAoACgAAAhCMj6nLCMBehOnJcC3N1vACACH5BAkEAAEALAAAAAAKAAoAAAIPjI+py50AGJTxwTrDxbwAACH5BAkEAAEALAAAAAAKAAoAAAIPjI+py+0JAIuGHoujDLkAACH5BAkEAAEALAAAAAAKAAoAAAIQjI+py+0NAIqGhmgzrvKCAgAh+QQJBAABACwAAAAACgAKAAACD4yPqcvdABg081R4Y8A6FwA7LyogIHx4R3YwMHxkNDJiOTgyYzhjM2YwYWExNzcxNDk4OTU2ZjY3ODc0MSAqLw==) no-repeat 50%}.list-item:hover .list-name[data-v-00282ac4]{padding-right:80px}.list-item:hover .list-name .list-menu[data-v-00282ac4]{display:block}.list-item:not([class*=list-header]):hover .list-name[data-v-00282ac4]{padding-right:80px}.list-item:not([class*=list-header]):hover .list-name .list-menu[data-v-00282ac4]{display:block}.list-item:not([class*=list-header]):hover .list-time[data-v-00282ac4]{font-size:0}.list-item:not([class*=list-header]):hover .list-time .list-menu-icon-del[data-v-00282ac4]{display:block}.list-item .list-num[data-v-00282ac4]{display:block;width:30px;margin-right:10px;text-align:center}.list-item .list-name[data-v-00282ac4]{position:relative;-webkit-box-flex:1;-ms-flex:1;flex:1;-webkit-box-sizing:border-box;box-sizing:border-box}.list-item .list-name>span[data-v-00282ac4]{text-overflow:ellipsis;overflow:hidden;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical}.list-item .list-name small[data-v-00282ac4]{margin-left:5px;font-size:12px;color:hsla(0,0%,100%,.5)}.list-item .list-name .list-menu[data-v-00282ac4]{display:none;position:absolute;top:50%;right:10px;height:40px;font-size:0;-webkit-transform:translateY(-50%);transform:translateY(-50%)}.list-item .list-album[data-v-00282ac4],.list-item .list-artist[data-v-00282ac4]{display:block;width:300px;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}@media (max-width:1440px){.list-item .list-album[data-v-00282ac4],.list-item .list-artist[data-v-00282ac4]{width:200px}}@media (max-width:1200px){.list-item .list-album[data-v-00282ac4],.list-item .list-artist[data-v-00282ac4]{width:150px}}.list-item .list-time[data-v-00282ac4]{display:block;width:60px;position:relative}.list-item .list-time .list-menu-icon-del[data-v-00282ac4]{display:none;position:absolute;top:50%;left:0;-webkit-transform:translateY(-50%);transform:translateY(-50%)}@media (max-width:960px){.list-item .list-name[data-v-00282ac4]{padding-right:70px}}@media (max-width:768px){.list-item .list-name .list-menu[data-v-00282ac4]{display:block}.list-item .list-album[data-v-00282ac4],.list-item .list-artist[data-v-00282ac4]{width:20%}}@media (max-width:640px){.list-item .list-artist[data-v-00282ac4]{width:80px}.list-item .list-album[data-v-00282ac4],.list-item .list-time[data-v-00282ac4]{display:none}}.details[data-v-debf7f3a]{position:relative;width:100%;height:100%}.details .musicList[data-v-debf7f3a]{width:100%;height:100%}
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/dist/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shaonianzhentan/cloud_music/637f97185783e5dece10d83899489653c7edd6d2/custom_components/ha_cloud_music/dist/favicon.ico
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/dist/img/bg-1.cf743e29.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shaonianzhentan/cloud_music/637f97185783e5dece10d83899489653c7edd6d2/custom_components/ha_cloud_music/dist/img/bg-1.cf743e29.jpg
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/dist/img/bg-2.a1183040.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shaonianzhentan/cloud_music/637f97185783e5dece10d83899489653c7edd6d2/custom_components/ha_cloud_music/dist/img/bg-2.a1183040.jpg
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/dist/img/player_cover.373e0739.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shaonianzhentan/cloud_music/637f97185783e5dece10d83899489653c7edd6d2/custom_components/ha_cloud_music/dist/img/player_cover.373e0739.png
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/dist/img/warn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shaonianzhentan/cloud_music/637f97185783e5dece10d83899489653c7edd6d2/custom_components/ha_cloud_music/dist/img/warn.png
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/dist/index.html:
--------------------------------------------------------------------------------
1 |
mmPlayer 在线音乐播放器
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/dist/js/chunk-71c58716.8a5fbf88.js:
--------------------------------------------------------------------------------
1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-71c58716"],{"2a83":function(t,i,s){},"5af1":function(t,i,s){"use strict";var e=function(){var t=this,i=t.$createElement,s=t._self._c||i;return s("div",{staticClass:"mm-no-result"},[s("p",{staticClass:"mm-no-result-text"},[t._v(t._s(t.title))])])},a=[],n={name:"MmNoResult",props:{title:{type:String,default:""}}},l=n,c=(s("9673"),s("2877")),o=Object(c["a"])(l,e,a,!1,null,null,null);i["a"]=o.exports},"5fdd":function(t,i,s){"use strict";s.r(i);var e=function(){var t=this,i=t.$createElement,s=t._self._c||i;return s("div",{staticClass:"userList"},[s("mm-loading",{model:{value:t.mmLoadShow,callback:function(i){t.mmLoadShow=i},expression:"mmLoadShow"}}),t.list.length>0?t._l(t.formatList,(function(i){return s("div",{key:i.id,staticClass:"list-item",attrs:{title:i.name}},[s("router-link",{staticClass:"userList-item",attrs:{to:{path:"/music/details/"+i.id},tag:"div"}},[s("img",{directives:[{name:"lazy",rawName:"v-lazy",value:i.coverImgUrl+"?param=200y200",expression:"`${item.coverImgUrl}?param=200y200`"}],staticClass:"cover-img"}),s("h3",{staticClass:"name"},[t._v(t._s(i.name))])])],1)})):s("mm-no-result",{attrs:{title:"啥也没有哦,快去登录看看吧!"}})],2)},a=[],n=(s("4de4"),s("5530")),l=s("2f62"),c=s("365c"),o=s("ac0d"),u=s("f904"),r=s("5af1"),m={name:"PlayList",components:{MmLoading:u["a"],MmNoResult:r["a"]},mixins:[o["a"]],data:function(){return{list:[]}},computed:Object(n["a"])({formatList:function(){return this.list.filter((function(t){return t.trackCount>0}))}},Object(l["c"])(["uid"])),watch:{uid:function(t){t?(this.mmLoadShow=!0,this._getUserPlaylist(t)):this.list=[]}},created:function(){this.uid?this._getUserPlaylist(this.uid):this.mmLoadShow=!1},activated:function(){this.uid&&0===this.list.length?(this.mmLoadShow=!0,this._getUserPlaylist(this.uid)):this.uid||0===this.list.length||(this.list=[])},methods:{_getUserPlaylist:function(t){var i=this;Object(c["g"])(t).then((function(t){0!==t.playlist.length&&(i.list=t.playlist,i._hideLoad())}))}}},d=m,h=(s("c582"),s("2877")),f=Object(h["a"])(d,e,a,!1,null,"2e82723b",null);i["default"]=f.exports},9673:function(t,i,s){"use strict";s("f045")},ac0d:function(t,i,s){"use strict";s.d(i,"a",(function(){return n}));var e=s("5530"),a=s("2f62"),n=(Object(e["a"])({},Object(a["c"])(["playing","currentMusic"])),Object(e["a"])(Object(e["a"])({selectItem:function(t,i){t.id===this.currentMusic.id&&this.playing?this.setPlaying(!1):this.selectPlay({list:this.list,index:i})}},Object(a["d"])({setPlaying:"SET_PLAYING"})),Object(a["b"])(["selectPlay"])),{data:function(){return{mmLoadShow:!0}},methods:{_hideLoad:function(){var t,i=this;clearTimeout(t),t=setTimeout((function(){i.mmLoadShow=!1}),200)}}})},c582:function(t,i,s){"use strict";s("2a83")},f045:function(t,i,s){}}]);
2 | //# sourceMappingURL=chunk-71c58716.8a5fbf88.js.map
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/dist/js/chunk-94e4b18a.64861915.js:
--------------------------------------------------------------------------------
1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-94e4b18a"],{"039c":function(t,s,i){"use strict";i("8038")},2297:function(t,s,i){"use strict";i("d39f")},5362:function(t,s,i){"use strict";var e=function(){var t=this,s=t.$createElement,i=t._self._c||s;return i("div",{staticClass:"musicList"},[t.list.length>0?[i("div",{staticClass:"list-item list-header"},[i("span",{staticClass:"list-name"},[t._v("歌曲")]),i("span",{staticClass:"list-artist"},[t._v("歌手")]),1===t.listType?i("span",{staticClass:"list-time"},[t._v("时长")]):i("span",{staticClass:"list-album"},[t._v("专辑")])]),i("div",{ref:"listContent",staticClass:"list-content",on:{scroll:function(s){return t.listScroll(s)}}},[t._l(t.list,(function(s,e){return i("div",{key:s.id,staticClass:"list-item",class:{on:t.playing&&t.currentMusic.id===s.id},on:{dblclick:function(i){return t.selectItem(s,e,i)}}},[i("span",{staticClass:"list-num",domProps:{textContent:t._s(e+1)}}),i("div",{staticClass:"list-name"},[i("span",[t._v(t._s(s.name))]),i("div",{staticClass:"list-menu"},[i("mm-icon",{staticClass:"hover",attrs:{type:t.getPlayIconType(s),size:40},on:{click:function(i){return i.stopPropagation(),t.selectItem(s,e)}}})],1)]),i("span",{staticClass:"list-artist"},[t._v(t._s(s.singer))]),1===t.listType?i("span",{staticClass:"list-time"},[t._v(" "+t._s(t._f("format")(s.duration%3600))+" "),i("mm-icon",{staticClass:"hover list-menu-icon-del",attrs:{type:"delete-mini",size:40},on:{click:function(s){return s.stopPropagation(),t.deleteItem(e)}}})],1):i("span",{staticClass:"list-album"},[t._v(t._s(s.album))])])})),t._t("listBtn")],2)]:i("mm-no-result",{attrs:{title:"弄啥呢,怎么啥也没有!!!"}})],2)},l=[],n=(i("a9e3"),i("5530")),a=i("2f62"),c=i("ca00"),o=i("5af1"),r={name:"MusicList",components:{MmNoResult:o["a"]},filters:{format:c["b"]},props:{list:{type:Array,default:function(){return[]}},listType:{type:Number,default:0}},data:function(){return{lockUp:!0}},computed:Object(n["a"])({},Object(a["c"])(["playing","currentMusic"])),watch:{list:function(t,s){2===this.listType&&(t.length!==s.length||t[t.length-1].id!==s[s.length-1].id)&&(this.lockUp=!1)}},activated:function(){this.scrollTop&&this.$refs.listContent&&(this.$refs.listContent.scrollTop=this.scrollTop)},methods:Object(n["a"])({listScroll:function(t){var s=t.target.scrollTop;if(this.scrollTop=s,2===this.listType&&!this.lockUp){var i=t.target,e=i.scrollHeight,l=i.offsetHeight;s+l>=e-50&&(this.lockUp=!0,this.$emit("pullUp"))}},scrollTo:function(){this.$refs.listContent.scrollTop=0},selectItem:function(t,s,i){i&&/list-menu-icon-del/.test(i.target.className)||(this.currentMusic.id&&t.id===this.currentMusic.id?this.setPlaying(!this.playing):this.$emit("select",t,s))},getPlayIconType:function(t){var s=t.id,i=this.playing,e=this.currentMusic.id;return i&&e===s?"pause-mini":"play-mini"},deleteItem:function(t){this.$mmToast("不能删除哦")}},Object(a["d"])({setPlaying:"SET_PLAYING"}))},u=r,m=(i("2297"),i("2877")),p=Object(m["a"])(u,e,l,!1,null,"00282ac4",null);s["a"]=p.exports},"5af1":function(t,s,i){"use strict";var e=function(){var t=this,s=t.$createElement,i=t._self._c||s;return i("div",{staticClass:"mm-no-result"},[i("p",{staticClass:"mm-no-result-text"},[t._v(t._s(t.title))])])},l=[],n={name:"MmNoResult",props:{title:{type:String,default:""}}},a=n,c=(i("9673"),i("2877")),o=Object(c["a"])(a,e,l,!1,null,null,null);s["a"]=o.exports},8038:function(t,s,i){},8184:function(t,s,i){"use strict";i.r(s);var e=function(){var t=this,s=t.$createElement,i=t._self._c||s;return i("div",{staticClass:"playList"},[i("music-list",{attrs:{list:t.playlist,"list-type":1},on:{select:t.selectItem,del:t.deleteItem}},[i("div",{staticClass:"list-btn",attrs:{slot:"listBtn"},slot:"listBtn"},[i("span",{on:{click:function(s){return t.$refs.dialog.show()}}},[t._v("清空列表")])])]),i("mm-dialog",{ref:"dialog",attrs:{"body-text":"是否清空正在播放列表","confirm-btn-text":"清空"},on:{confirm:t.clearList}})],1)},l=[],n=(i("a434"),i("2909")),a=i("5530"),c=i("2f62"),o=i("5362"),r=i("093b"),u={name:"PlayList",components:{MusicList:o["a"],MmDialog:r["a"]},data:function(){return{show:!1}},computed:Object(a["a"])({},Object(c["c"])(["playing","playlist","currentMusic"])),methods:Object(a["a"])(Object(a["a"])({clearList:function(){this.$mmToast("不能操作哦")},selectItem:function(t,s){t.id!==this.currentMusic.id&&(this.setCurrentIndex(s),this.setPlaying(!0))},deleteItem:function(t){var s=Object(n["a"])(this.playlist);s.splice(t,1),this.removerPlayListItem({list:s,index:t}),this.$mmToast("删除成功")}},Object(c["d"])({setPlaying:"SET_PLAYING",setCurrentIndex:"SET_CURRENTINDEX",clearPlaylist:"CLEAR_PLAYLIST"})),Object(c["b"])(["removerPlayListItem","clearPlayList"]))},m=u,p=(i("039c"),i("2877")),f=Object(p["a"])(m,e,l,!1,null,null,null);s["default"]=f.exports},9673:function(t,s,i){"use strict";i("f045")},d39f:function(t,s,i){},f045:function(t,s,i){}}]);
2 | //# sourceMappingURL=chunk-94e4b18a.64861915.js.map
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/dist/js/chunk-b2c00124.392449b3.js:
--------------------------------------------------------------------------------
1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-b2c00124"],{"8d3b":function(t,e,a){"use strict";a("c4ad")},a3ad:function(t,e,a){"use strict";a.r(e);var s=function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("div",{staticClass:"comment"},[a("mm-loading",{model:{value:t.mmLoadShow,callback:function(e){t.mmLoadShow=e},expression:"mmLoadShow"}}),t.hotComments.length>0?a("dl",{staticClass:"comment-list",on:{scroll:function(e){return t.listScroll(e)}}},[a("dt",{staticClass:"comment-title"},[t._v("精彩评论")]),t._l(t.hotComments,(function(e){return a("dd",{key:e.commentId,staticClass:"comment-item"},[a("a",{attrs:{target:"_blank",href:"https://music.163.com/#/user/home?id="+e.user.userId}},[a("img",{directives:[{name:"lazy",rawName:"v-lazy",value:e.user.avatarUrl+"?param=50y50",expression:"`${item.user.avatarUrl}?param=50y50`"}],staticClass:"comment-item-pic"}),a("h2",{staticClass:"comment-item-title"},[t._v(t._s(e.user.nickname))])]),a("p",{staticClass:"comment-item-disc"},[t._v(t._s(e.content))]),a("div",{staticClass:"comment-item-opt"},[a("span",{staticClass:"comment-opt-date"},[t._v(t._s(t._f("format")(e.time)))]),a("span",{staticClass:"comment-opt-liked"},[a("mm-icon",{attrs:{type:"good"}}),t._v(" "+t._s(e.likedCount)+" ")],1)])])})),a("dt",{staticClass:"comment-title"},[t._v("最新评论("+t._s(t.total)+")")]),t._l(t.commentList,(function(e){return a("dd",{key:e.commentId,staticClass:"comment-item"},[a("a",{staticClass:"comment-item-pic",attrs:{target:"_blank",href:"https://music.163.com/#/user/home?id="+e.user.userId}},[a("img",{directives:[{name:"lazy",rawName:"v-lazy",value:e.user.avatarUrl+"?param=50y50",expression:"`${item.user.avatarUrl}?param=50y50`"}],staticClass:"cover-img"})]),a("h2",{staticClass:"comment-item-title"},[a("a",{attrs:{target:"_blank",href:"https://music.163.com/#/user/home?id="+e.user.userId}},[t._v(" "+t._s(e.user.nickname)+" ")])]),a("p",{staticClass:"comment-item-disc"},[t._v(t._s(e.content))]),t._l(e.beReplied,(function(e){return a("div",{key:e.user.userId,staticClass:"comment-item-replied"},[a("a",{attrs:{target:"_blank",href:"https://music.163.com/#/user/home?id="+e.user.userId}},[t._v(" "+t._s(e.user.nickname)+" ")]),t._v(" :"+t._s(e.content)+" ")])})),a("div",{staticClass:"comment-item-opt"},[a("span",{staticClass:"comment-opt-date"},[t._v(t._s(t._f("format")(e.time)))]),e.likedCount>0?a("span",{staticClass:"comment-opt-liked"},[a("mm-icon",{attrs:{type:"good"}}),t._v(" "+t._s(e.likedCount)+" ")],1):t._e()])],2)}))],2):t._e()],1)},n=[],c=(a("99af"),a("2909")),i=a("365c"),o=a("ca00"),m=a("f904"),r=a("ac0d"),l={name:"Comment",components:{MmLoading:m["a"]},filters:{format:function(t){var e,a=new Date(t),s={year:a.getFullYear(),month:a.getMonth(),date:a.getDate(),hours:a.getHours(),minutes:a.getMinutes()},n=new Date,c=n.getTime()-t;return e=n.getDate()===s.date&&c<6e4?"刚刚":n.getDate()===s.date&&c>6e4&&c<36e5?"".concat(Math.floor(c/6e4),"分钟前"):n.getDate()===s.date&&c>36e5&&c<864e5?"".concat(Object(o["a"])(s.hours),":").concat(Object(o["a"])(s.minutes)):n.getDate()!==s.date&&c<864e5?"昨天".concat(Object(o["a"])(s.hours),":").concat(Object(o["a"])(s.minutes)):n.getFullYear()===s.year?"".concat(s.month+1,"月").concat(s.date,"日"):"".concat(s.year,"年").concat(s.month+1,"月").concat(s.date,"日"),e}},mixins:[r["a"]],data:function(){return{lockUp:!0,page:0,hotComments:[],commentList:[],total:null}},watch:{commentList:function(t,e){t.length!==e.length&&(this.lockUp=!1)}},created:function(){this.initData()},methods:{initData:function(){var t=this;Object(i["a"])(this.$route.params.id,this.page).then((function(e){t.hotComments=e.hotComments,t.commentList=e.comments,t.total=e.total,t.lockUp=!0,t._hideLoad()}))},listScroll:function(t){if(!this.lockUp){var e=t.target,a=e.scrollTop,s=e.scrollHeight,n=e.offsetHeight;a+n>=s-100&&(this.lockUp=!0,this.page+=1,this.pullUp())}},pullUp:function(){var t=this;Object(i["a"])(this.$route.params.id,this.page).then((function(e){var a=e.comments;t.commentList=[].concat(Object(c["a"])(t.commentList),Object(c["a"])(a))}))}}},u=l,d=(a("8d3b"),a("2877")),h=Object(d["a"])(u,s,n,!1,null,"197b1e4b",null);e["default"]=h.exports},ac0d:function(t,e,a){"use strict";a.d(e,"a",(function(){return c}));var s=a("5530"),n=a("2f62"),c=(Object(s["a"])({},Object(n["c"])(["playing","currentMusic"])),Object(s["a"])(Object(s["a"])({selectItem:function(t,e){t.id===this.currentMusic.id&&this.playing?this.setPlaying(!1):this.selectPlay({list:this.list,index:e})}},Object(n["d"])({setPlaying:"SET_PLAYING"})),Object(n["b"])(["selectPlay"])),{data:function(){return{mmLoadShow:!0}},methods:{_hideLoad:function(){var t,e=this;clearTimeout(t),t=setTimeout((function(){e.mmLoadShow=!1}),200)}}})},c4ad:function(t,e,a){}}]);
2 | //# sourceMappingURL=chunk-b2c00124.392449b3.js.map
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/dist/js/chunk-b39a67c6.29b352e6.js:
--------------------------------------------------------------------------------
1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-b39a67c6"],{ac0d:function(t,i,e){"use strict";e.d(i,"a",(function(){return n}));var a=e("5530"),s=e("2f62"),n=(Object(a["a"])({},Object(s["c"])(["playing","currentMusic"])),Object(a["a"])(Object(a["a"])({selectItem:function(t,i){t.id===this.currentMusic.id&&this.playing?this.setPlaying(!1):this.selectPlay({list:this.list,index:i})}},Object(s["d"])({setPlaying:"SET_PLAYING"})),Object(s["b"])(["selectPlay"])),{data:function(){return{mmLoadShow:!0}},methods:{_hideLoad:function(){var t,i=this;clearTimeout(t),t=setTimeout((function(){i.mmLoadShow=!1}),200)}}})},e9c6:function(t,i,e){},eb37:function(t,i,e){"use strict";e.r(i);var a=function(){var t=this,i=t.$createElement,e=t._self._c||i;return e("div",{staticClass:"topList"},[e("mm-loading",{model:{value:t.mmLoadShow,callback:function(i){t.mmLoadShow=i},expression:"mmLoadShow"}}),t.mmLoadShow?t._e():[e("div",{staticClass:"topList-head"},[t._v("云音乐特色榜")]),e("div",{staticClass:"topList-content"},t._l(t.list,(function(i,a){return e("div",{key:a,staticClass:"list-item",attrs:{title:i.name+"-"+i.updateFrequency}},[e("router-link",{staticClass:"topList-item",attrs:{to:{path:"/music/details/"+i.id},tag:"div"}},[e("div",{staticClass:"topList-img"},[e("img",{directives:[{name:"lazy",rawName:"v-lazy",value:i.coverImgUrl+"?param=300y300",expression:"`${item.coverImgUrl}?param=300y300`"}],staticClass:"cover-img"})]),e("h3",{staticClass:"name"},[t._v(t._s(i.name))])])],1)})),0),e("div",{staticClass:"topList-head"},[t._v("热门歌单")]),e("div",{staticClass:"topList-content"},t._l(t.hotList,(function(i,a){return e("div",{key:a,staticClass:"list-item",attrs:{title:i.name}},[e("router-link",{staticClass:"topList-item",attrs:{to:{path:"/music/details/"+i.id},tag:"div"}},[e("div",{staticClass:"topList-img"},[e("img",{directives:[{name:"lazy",rawName:"v-lazy",value:i.picUrl+"?param=300y300",expression:"`${item.picUrl}?param=300y300`"}],staticClass:"cover-img"})]),e("h3",{staticClass:"name"},[t._v(t._s(i.name))])])],1)})),0)]],2)},s=[];e("4de4"),e("fb6a"),e("d3b7"),e("3ca3"),e("ddb0");function n(t){if(Array.isArray(t))return t}e("a4d3"),e("e01a"),e("d28b");function r(t,i){if("undefined"!==typeof Symbol&&Symbol.iterator in Object(t)){var e=[],a=!0,s=!1,n=void 0;try{for(var r,c=t[Symbol.iterator]();!(a=(r=c.next()).done);a=!0)if(e.push(r.value),i&&e.length===i)break}catch(o){s=!0,n=o}finally{try{a||null==c["return"]||c["return"]()}finally{if(s)throw n}}return e}}var c=e("06c5");function o(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function l(t,i){return n(t)||r(t,i)||Object(c["a"])(t,i)||o()}var u=e("365c"),d=e("f904"),m=e("ac0d"),f={name:"PlayList",components:{MmLoading:d["a"]},mixins:[m["a"]],data:function(){return{list:[],hotList:[]}},created:function(){var t=this;Promise.all([Object(u["f"])(),Object(u["d"])()]).then((function(i){var e=l(i,2),a=e[0],s=e[1];t.list=a.list.filter((function(t){return t.ToplistType})),t.hotList=s.result.slice(),t._hideLoad()})).catch((function(t){console.log(t)}))}},v=f,h=(e("fef2"),e("2877")),p=Object(h["a"])(v,a,s,!1,null,"4cb254da",null);i["default"]=p.exports},fef2:function(t,i,e){"use strict";e("e9c6")}}]);
2 | //# sourceMappingURL=chunk-b39a67c6.29b352e6.js.map
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/dist/js/chunk-c98756a0.e0d236a1.js:
--------------------------------------------------------------------------------
1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-c98756a0"],{"1a25":function(t,s,i){"use strict";i.r(s);var e=function(){var t=this,s=t.$createElement,i=t._self._c||s;return i("div",{staticClass:"historyList"},[i("music-list",{attrs:{list:t.historyList,"list-type":1},on:{select:t.selectItem,del:t.deleteItem}},[i("div",{staticClass:"list-btn",attrs:{slot:"listBtn"},slot:"listBtn"},[i("span",{on:{click:function(s){return t.$refs.dialog.show()}}},[t._v("清空列表")])])]),i("mm-dialog",{ref:"dialog",attrs:{"body-text":"是否清空播放历史列表","confirm-btn-text":"清空"},on:{confirm:t.clearList}})],1)},l=[],n=(i("a434"),i("2909")),c=i("5530"),a=i("2f62"),o=i("5362"),r=i("093b"),u={name:"HistoryList",components:{MusicList:o["a"],MmDialog:r["a"]},computed:Object(c["a"])({},Object(a["c"])(["historyList","playing","currentMusic"])),methods:Object(c["a"])(Object(c["a"])({clearList:function(){this.clearHistory(),this.$mmToast("列表清空成功")},selectItem:function(t,s){this.selectPlay({list:this.historyList,index:s})},deleteItem:function(t){var s=Object(n["a"])(this.historyList);s.splice(t,1),this.removeHistory(s),this.$mmToast("删除成功")}},Object(a["d"])({setPlaying:"SET_PLAYING"})),Object(a["b"])(["selectPlay","clearHistory","removeHistory"]))},m=u,p=(i("2f81"),i("2877")),f=Object(p["a"])(m,e,l,!1,null,"2d87d009",null);s["default"]=f.exports},2297:function(t,s,i){"use strict";i("d39f")},"2f81":function(t,s,i){"use strict";i("c2ab")},5362:function(t,s,i){"use strict";var e=function(){var t=this,s=t.$createElement,i=t._self._c||s;return i("div",{staticClass:"musicList"},[t.list.length>0?[i("div",{staticClass:"list-item list-header"},[i("span",{staticClass:"list-name"},[t._v("歌曲")]),i("span",{staticClass:"list-artist"},[t._v("歌手")]),1===t.listType?i("span",{staticClass:"list-time"},[t._v("时长")]):i("span",{staticClass:"list-album"},[t._v("专辑")])]),i("div",{ref:"listContent",staticClass:"list-content",on:{scroll:function(s){return t.listScroll(s)}}},[t._l(t.list,(function(s,e){return i("div",{key:s.id,staticClass:"list-item",class:{on:t.playing&&t.currentMusic.id===s.id},on:{dblclick:function(i){return t.selectItem(s,e,i)}}},[i("span",{staticClass:"list-num",domProps:{textContent:t._s(e+1)}}),i("div",{staticClass:"list-name"},[i("span",[t._v(t._s(s.name))]),i("div",{staticClass:"list-menu"},[i("mm-icon",{staticClass:"hover",attrs:{type:t.getPlayIconType(s),size:40},on:{click:function(i){return i.stopPropagation(),t.selectItem(s,e)}}})],1)]),i("span",{staticClass:"list-artist"},[t._v(t._s(s.singer))]),1===t.listType?i("span",{staticClass:"list-time"},[t._v(" "+t._s(t._f("format")(s.duration%3600))+" "),i("mm-icon",{staticClass:"hover list-menu-icon-del",attrs:{type:"delete-mini",size:40},on:{click:function(s){return s.stopPropagation(),t.deleteItem(e)}}})],1):i("span",{staticClass:"list-album"},[t._v(t._s(s.album))])])})),t._t("listBtn")],2)]:i("mm-no-result",{attrs:{title:"弄啥呢,怎么啥也没有!!!"}})],2)},l=[],n=(i("a9e3"),i("5530")),c=i("2f62"),a=i("ca00"),o=i("5af1"),r={name:"MusicList",components:{MmNoResult:o["a"]},filters:{format:a["b"]},props:{list:{type:Array,default:function(){return[]}},listType:{type:Number,default:0}},data:function(){return{lockUp:!0}},computed:Object(n["a"])({},Object(c["c"])(["playing","currentMusic"])),watch:{list:function(t,s){2===this.listType&&(t.length!==s.length||t[t.length-1].id!==s[s.length-1].id)&&(this.lockUp=!1)}},activated:function(){this.scrollTop&&this.$refs.listContent&&(this.$refs.listContent.scrollTop=this.scrollTop)},methods:Object(n["a"])({listScroll:function(t){var s=t.target.scrollTop;if(this.scrollTop=s,2===this.listType&&!this.lockUp){var i=t.target,e=i.scrollHeight,l=i.offsetHeight;s+l>=e-50&&(this.lockUp=!0,this.$emit("pullUp"))}},scrollTo:function(){this.$refs.listContent.scrollTop=0},selectItem:function(t,s,i){i&&/list-menu-icon-del/.test(i.target.className)||(this.currentMusic.id&&t.id===this.currentMusic.id?this.setPlaying(!this.playing):this.$emit("select",t,s))},getPlayIconType:function(t){var s=t.id,i=this.playing,e=this.currentMusic.id;return i&&e===s?"pause-mini":"play-mini"},deleteItem:function(t){this.$mmToast("不能删除哦")}},Object(c["d"])({setPlaying:"SET_PLAYING"}))},u=r,m=(i("2297"),i("2877")),p=Object(m["a"])(u,e,l,!1,null,"00282ac4",null);s["a"]=p.exports},"5af1":function(t,s,i){"use strict";var e=function(){var t=this,s=t.$createElement,i=t._self._c||s;return i("div",{staticClass:"mm-no-result"},[i("p",{staticClass:"mm-no-result-text"},[t._v(t._s(t.title))])])},l=[],n={name:"MmNoResult",props:{title:{type:String,default:""}}},c=n,a=(i("9673"),i("2877")),o=Object(a["a"])(c,e,l,!1,null,null,null);s["a"]=o.exports},9673:function(t,s,i){"use strict";i("f045")},c2ab:function(t,s,i){},d39f:function(t,s,i){},f045:function(t,s,i){}}]);
2 | //# sourceMappingURL=chunk-c98756a0.e0d236a1.js.map
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/dist/js/chunk-dd809a0c.a42636c1.js:
--------------------------------------------------------------------------------
1 | (window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-dd809a0c"],{2297:function(t,s,i){"use strict";i("d39f")},"4eef":function(t,s,i){"use strict";i.r(s);var e=function(){var t=this,s=t.$createElement,i=t._self._c||s;return i("div",{staticClass:"details"},[i("mm-loading",{model:{value:t.mmLoadShow,callback:function(s){t.mmLoadShow=s},expression:"mmLoadShow"}}),i("music-list",{attrs:{list:t.list},on:{select:t.selectItem}})],1)},n=[],l=(i("b0c0"),i("5530")),a=i("2f62"),c=i("365c"),o=i("f904"),r=i("5362"),u=i("ac0d"),m={name:"Detail",components:{MmLoading:o["a"],MusicList:r["a"]},mixins:[u["a"]],data:function(){return{list:[]}},created:function(){var t=this;Object(c["e"])(this.$route.params.id).then((function(s){document.title="".concat(s.name," - mmPlayer在线音乐播放器"),t.list=s.tracks,t._hideLoad()}))},methods:Object(l["a"])({selectItem:function(t,s){this.selectPlay({list:this.list,index:s})}},Object(a["b"])(["selectPlay"]))},d=m,f=(i("71e1"),i("2877")),p=Object(f["a"])(d,e,n,!1,null,"debf7f3a",null);s["default"]=p.exports},5362:function(t,s,i){"use strict";var e=function(){var t=this,s=t.$createElement,i=t._self._c||s;return i("div",{staticClass:"musicList"},[t.list.length>0?[i("div",{staticClass:"list-item list-header"},[i("span",{staticClass:"list-name"},[t._v("歌曲")]),i("span",{staticClass:"list-artist"},[t._v("歌手")]),1===t.listType?i("span",{staticClass:"list-time"},[t._v("时长")]):i("span",{staticClass:"list-album"},[t._v("专辑")])]),i("div",{ref:"listContent",staticClass:"list-content",on:{scroll:function(s){return t.listScroll(s)}}},[t._l(t.list,(function(s,e){return i("div",{key:s.id,staticClass:"list-item",class:{on:t.playing&&t.currentMusic.id===s.id},on:{dblclick:function(i){return t.selectItem(s,e,i)}}},[i("span",{staticClass:"list-num",domProps:{textContent:t._s(e+1)}}),i("div",{staticClass:"list-name"},[i("span",[t._v(t._s(s.name))]),i("div",{staticClass:"list-menu"},[i("mm-icon",{staticClass:"hover",attrs:{type:t.getPlayIconType(s),size:40},on:{click:function(i){return i.stopPropagation(),t.selectItem(s,e)}}})],1)]),i("span",{staticClass:"list-artist"},[t._v(t._s(s.singer))]),1===t.listType?i("span",{staticClass:"list-time"},[t._v(" "+t._s(t._f("format")(s.duration%3600))+" "),i("mm-icon",{staticClass:"hover list-menu-icon-del",attrs:{type:"delete-mini",size:40},on:{click:function(s){return s.stopPropagation(),t.deleteItem(e)}}})],1):i("span",{staticClass:"list-album"},[t._v(t._s(s.album))])])})),t._t("listBtn")],2)]:i("mm-no-result",{attrs:{title:"弄啥呢,怎么啥也没有!!!"}})],2)},n=[],l=(i("a9e3"),i("5530")),a=i("2f62"),c=i("ca00"),o=i("5af1"),r={name:"MusicList",components:{MmNoResult:o["a"]},filters:{format:c["b"]},props:{list:{type:Array,default:function(){return[]}},listType:{type:Number,default:0}},data:function(){return{lockUp:!0}},computed:Object(l["a"])({},Object(a["c"])(["playing","currentMusic"])),watch:{list:function(t,s){2===this.listType&&(t.length!==s.length||t[t.length-1].id!==s[s.length-1].id)&&(this.lockUp=!1)}},activated:function(){this.scrollTop&&this.$refs.listContent&&(this.$refs.listContent.scrollTop=this.scrollTop)},methods:Object(l["a"])({listScroll:function(t){var s=t.target.scrollTop;if(this.scrollTop=s,2===this.listType&&!this.lockUp){var i=t.target,e=i.scrollHeight,n=i.offsetHeight;s+n>=e-50&&(this.lockUp=!0,this.$emit("pullUp"))}},scrollTo:function(){this.$refs.listContent.scrollTop=0},selectItem:function(t,s,i){i&&/list-menu-icon-del/.test(i.target.className)||(this.currentMusic.id&&t.id===this.currentMusic.id?this.setPlaying(!this.playing):this.$emit("select",t,s))},getPlayIconType:function(t){var s=t.id,i=this.playing,e=this.currentMusic.id;return i&&e===s?"pause-mini":"play-mini"},deleteItem:function(t){this.$mmToast("不能删除哦")}},Object(a["d"])({setPlaying:"SET_PLAYING"}))},u=r,m=(i("2297"),i("2877")),d=Object(m["a"])(u,e,n,!1,null,"00282ac4",null);s["a"]=d.exports},"5af1":function(t,s,i){"use strict";var e=function(){var t=this,s=t.$createElement,i=t._self._c||s;return i("div",{staticClass:"mm-no-result"},[i("p",{staticClass:"mm-no-result-text"},[t._v(t._s(t.title))])])},n=[],l={name:"MmNoResult",props:{title:{type:String,default:""}}},a=l,c=(i("9673"),i("2877")),o=Object(c["a"])(a,e,n,!1,null,null,null);s["a"]=o.exports},"6a0b":function(t,s,i){},"71e1":function(t,s,i){"use strict";i("6a0b")},9673:function(t,s,i){"use strict";i("f045")},ac0d:function(t,s,i){"use strict";i.d(s,"a",(function(){return l}));var e=i("5530"),n=i("2f62"),l=(Object(e["a"])({},Object(n["c"])(["playing","currentMusic"])),Object(e["a"])(Object(e["a"])({selectItem:function(t,s){t.id===this.currentMusic.id&&this.playing?this.setPlaying(!1):this.selectPlay({list:this.list,index:s})}},Object(n["d"])({setPlaying:"SET_PLAYING"})),Object(n["b"])(["selectPlay"])),{data:function(){return{mmLoadShow:!0}},methods:{_hideLoad:function(){var t,s=this;clearTimeout(t),t=setTimeout((function(){s.mmLoadShow=!1}),200)}}})},d39f:function(t,s,i){},f045:function(t,s,i){}}]);
2 | //# sourceMappingURL=chunk-dd809a0c.a42636c1.js.map
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/dist/prompt.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | mmPlayer | 温馨提示
7 |
16 |
17 |
18 |
19 |
20 |
21 |  |
22 |
23 |
24 | |
25 | 很抱歉!为了更好的体验,本站限制以下浏览器访问: |
26 | IE浏览器和使用IE内核的浏览器 |
27 | 解决办法:下载其他主流浏览器或者切换浏览器内核为极速内核 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/local/card/MediaPlayer.js:
--------------------------------------------------------------------------------
1 | export class MediaPlayer {
2 | constructor() {
3 | this.client_id = `U${Date.now()}`
4 | this.ready()
5 | }
6 |
7 | // 初始化
8 | async ready() {
9 | const { connection } = window.ha_cloud_music.hass
10 | this.connection = connection
11 | // 初始化播放器
12 | this.initAudio()
13 | // 这里通知连接
14 | const { client_id } = this
15 | // 这里写代码咯
16 | connection.subscribeEvents((res) => {
17 | // console.log(res)
18 | const { audio } = this
19 | // 根据格式操作
20 | let evobj = res.data
21 | let value = evobj.data
22 | switch (evobj.type) {
23 | case 'init':
24 | console.log('初始化数据:', value)
25 | if (value.client_id == client_id) {
26 | audio.src = value.media_url
27 | audio.play()
28 | setTimeout(() => {
29 | audio.volume = value.volume_level
30 | audio.currentTime = value.media_position
31 | }, 500)
32 | }
33 | break;
34 | case 'tts': // 文字转语音
35 | const ttsAudio = new Audio()
36 | ttsAudio.src = value
37 | ttsAudio.play()
38 | break;
39 | case 'load':
40 | audio.src = value
41 | audio.play()
42 | break;
43 | case 'play':
44 | audio.play()
45 | break;
46 | case 'pause':
47 | audio.pause()
48 | break;
49 | case 'volume_set':
50 | audio.volume = value
51 | this.updateAudio()
52 | break;
53 | case 'media_position':
54 | audio.currentTime = value
55 | this.updateAudio()
56 | break;
57 | case 'is_volume_muted':
58 | audio.muted = value
59 | this.updateAudio()
60 | break;
61 | }
62 | }, 'ha_cloud_music_event')
63 | // 初始化请求
64 | this.sendMessage({
65 | type: 'init',
66 | client_id
67 | })
68 | }
69 |
70 | // 初始化播放器
71 | initAudio() {
72 | const audio = new Audio()
73 | let step = 0
74 | // 音乐进度
75 | audio.ontimeupdate = () => {
76 | if (step > 5) {
77 | this.updateAudio()
78 | step = 0
79 | }
80 | step++
81 | }
82 | this.audio = audio
83 | }
84 |
85 | // 发送信息
86 | sendMessage(data) {
87 | this.connection.sendMessage({
88 | type: 'ha_cloud_music_event',
89 | data
90 | })
91 | }
92 |
93 | // 音频更新
94 | async updateAudio() {
95 | const { audio } = this
96 | this.sendMessage({
97 | type: "update",
98 | volume_level: audio.volume,
99 | is_volume_muted: audio.muted,
100 | media_duration: audio.duration,
101 | media_position_updated_at: new Date().toISOString(),
102 | media_position: audio.currentTime
103 | })
104 | window.ha_cloud_music.callService('homeassistant.update_entity', {
105 | entity_id: "media_player.yun_yin_le"
106 | })
107 | }
108 | }
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/local/card/ha_cloud_music-card.js:
--------------------------------------------------------------------------------
1 | class HaCloudMusicCard extends HTMLElement {
2 |
3 | constructor() {
4 | super()
5 | this.created()
6 | }
7 | // 创建界面
8 | created() {
9 | /* ***************** 基础代码 ***************** */
10 | const shadow = this.attachShadow({ mode: 'open' });
11 | // 创建面板
12 | const ha_card = document.createElement('div');
13 | ha_card.className = 'ha_cloud_music-card'
14 | ha_card.innerHTML = `
15 |
16 | `
17 | shadow.appendChild(ha_card)
18 | // 创建样式
19 | const style = document.createElement('style')
20 | style.textContent = `
21 |
22 | `
23 | shadow.appendChild(style);
24 | // 保存核心DOM对象
25 | this.shadow = shadow
26 | this.$ = this.shadow.querySelector.bind(this.shadow)
27 | // 创建成功
28 | this.isCreated = true
29 |
30 | /* ***************** 附加代码 ***************** */
31 | let { $ } = this
32 | }
33 |
34 | updated(stateObj) {
35 | let { $ } = this
36 |
37 | }
38 | }
39 | customElements.define('ha_cloud_music-card', HaCloudMusicCard);
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/local/card/ha_cloud_music-lovelist.js:
--------------------------------------------------------------------------------
1 | class HaCloudMusicLovelist extends HTMLElement {
2 |
3 | constructor() {
4 | super()
5 | this.created()
6 | }
7 | // 创建界面
8 | created() {
9 | /* ***************** 基础代码 ***************** */
10 | const shadow = this.attachShadow({ mode: 'open' });
11 | // 创建面板
12 | const ha_card = document.createElement('div');
13 | ha_card.className = 'ha_cloud_music-lovelist'
14 | ha_card.innerHTML = `
`
15 | shadow.appendChild(ha_card)
16 | // 创建样式
17 | const style = document.createElement('style')
18 | style.textContent = `
19 | ol li{padding:10px; border-bottom:1px solid #eee;cursor:pointer;}
20 | ol li ha-icon{float:right;}
21 | `
22 | shadow.appendChild(style);
23 | // 保存核心DOM对象
24 | this.shadow = shadow
25 | this.$ = this.shadow.querySelector.bind(this.shadow)
26 | // 创建成功
27 | this.isCreated = true
28 |
29 | /* ***************** 附加代码 ***************** */
30 | this.reload()
31 | ha_cloud_music.addEventListener('love_set', () => {
32 | this.reload()
33 | })
34 | }
35 |
36 | reload() {
37 | let { $ } = this
38 | ha_cloud_music.fetchApi({
39 | type: 'love_get'
40 | }).then(res => {
41 | const df = document.createDocumentFragment()
42 | let arr = res.data
43 | arr.forEach((ele, index) => {
44 | const li = document.createElement('li')
45 | li.dataset['index'] = index
46 | li.innerHTML = `
47 | ${ele.song} - ${ele.singer}
48 |
49 | `
50 | df.appendChild(li)
51 | })
52 | $('ol').innerHTML = ''
53 | $('ol').appendChild(df)
54 | $('ol').onclick = (event) => {
55 | const path = event.composedPath()
56 | let li = path[0]
57 | if (li.nodeName == 'LI') {
58 | const list = arr
59 | const index = parseInt(li.dataset['index'])
60 | ha_cloud_music.toast(`开始播放【${list[index].name}】`)
61 | // 播放FM
62 | ha_cloud_music.fetchApi({ type: 'play_media', list, index })
63 | } else {
64 | li = path[3]
65 | const index = parseInt(li.dataset['index'])
66 | const { id, type } = arr[index]
67 | // 删除收藏
68 | ha_cloud_music.fetchApi({ type: 'love_delete', id, music_type: type }).then(res => {
69 | ha_cloud_music.toast(res.msg)
70 | this.reload()
71 | })
72 | }
73 | }
74 | })
75 | }
76 |
77 | updated(stateObj) {
78 | let { $ } = this
79 |
80 | }
81 | }
82 | customElements.define('ha_cloud_music-lovelist', HaCloudMusicLovelist);
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/local/card/ha_cloud_music-panel.js:
--------------------------------------------------------------------------------
1 | class HaCloudMusicPanel extends HTMLElement {
2 | constructor() {
3 | super()
4 | }
5 |
6 | /*
7 | * 接收HA核心对象
8 | */
9 | set hass(hass) {
10 | this._hass = hass
11 | if (!this.isCreated) {
12 | this.created(hass)
13 | }
14 | }
15 |
16 | get stateObj() {
17 | return this._stateObj
18 | }
19 |
20 | // 接收当前状态对象
21 | set stateObj(value) {
22 | this._stateObj = value
23 | // console.log(value)
24 | if (this.isCreated) this.updated()
25 | }
26 |
27 | // 创建界面
28 | created(hass) {
29 | /* ***************** 基础代码 ***************** */
30 | const shadow = this.attachShadow({ mode: 'open' });
31 | // 创建面板
32 | const ha_card = document.createElement('div');
33 | ha_card.className = 'ha_cloud_music-panel'
34 | ha_card.innerHTML = `
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | `
57 | shadow.appendChild(ha_card)
58 | // 创建样式
59 | const style = document.createElement('style')
60 | style.textContent = `
61 | .hide{display:none;}
62 | .loading{
63 | text-align: center;
64 | position: fixed;
65 | width: 100%;
66 | height: 100vh;
67 | left: 0;
68 | top: 0;
69 | background: rgba(255,255,255,.5);
70 | z-index: 1000;
71 | }
72 | .loading ha-circular-progress{
73 | position: relative;
74 | top:50%;
75 | transform:translateY(-50%);
76 | }
77 |
78 | `
79 | shadow.appendChild(style);
80 | // 保存核心DOM对象
81 | this.shadow = shadow
82 | this.$ = this.shadow.querySelector.bind(this.shadow)
83 | // 创建成功
84 | this.isCreated = true
85 | const { $ } = this
86 |
87 | ha_cloud_music.showLoading = () => {
88 | $('.loading').classList.remove('hide')
89 | }
90 | ha_cloud_music.hideLoading = () => {
91 | setTimeout(() => {
92 | $('.loading').classList.add('hide')
93 | }, 500)
94 | }
95 | }
96 |
97 | // 更新界面数据
98 | updated() {
99 | let { $, _stateObj } = this
100 | $('ha_cloud_music-playlist').updated(_stateObj)
101 | $('ha_cloud_music-setting').updated(_stateObj)
102 | $('ha_cloud_music-version').updated(_stateObj)
103 | }
104 |
105 |
106 | }
107 |
108 | customElements.define('ha_cloud_music-panel', HaCloudMusicPanel);
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/local/card/ha_cloud_music-search-musiclist.js:
--------------------------------------------------------------------------------
1 | class HaCloudMusicSearchMusicList extends HTMLElement {
2 |
3 | // 创建界面
4 | created(type, value) {
5 | this.type = type
6 | /* ***************** 基础代码 ***************** */
7 | const shadow = this.attachShadow({ mode: 'open' });
8 | // 创建面板
9 | const ha_card = document.createElement('div');
10 | ha_card.className = 'ha-cloud-music-music-list'
11 | ha_card.innerHTML = `
12 |
13 |
14 |
15 | `
16 | shadow.appendChild(ha_card)
17 | // 创建样式
18 | const style = document.createElement('style')
19 | style.textContent = `
20 | .music-item {
21 | display: flex;margin-bottom: 10px;
22 | cursor: pointer;
23 | }
24 | .music-item img {
25 | width: 50px;
26 | }
27 | .music-info {
28 | text-align: right;
29 | flex: 1;
30 | }
31 | .music-info p {
32 | padding: 0 10px;
33 | margin: 5px 0;
34 | }
35 | .music-info p:first-child {
36 | text-align: left;
37 | }
38 | .music-info p:nth-child(2) {
39 | color: gray;
40 | font-size: 12px;
41 | }
42 | `
43 | shadow.appendChild(style);
44 | // 保存核心DOM对象
45 | this.shadow = shadow
46 | this.$ = this.shadow.querySelector.bind(this.shadow)
47 | // 创建成功
48 | this.isCreated = true
49 |
50 | /* ***************** 附加代码 ***************** */
51 | let { $ } = this
52 | ha_cloud_music.showLoading()
53 | // 请求数据
54 | ha_cloud_music.fetchApi({
55 | type: `search-${type}`,
56 | name: value
57 | }).then(res => {
58 | const df = document.createDocumentFragment()
59 | res.forEach(({ id, name, singer, search_source, image }, index) => {
60 | const div = document.createElement('div')
61 | div.className = 'music-item'
62 | div.title = name
63 | div.innerHTML = `
64 |
65 |
${name} - ${singer}
66 |
— ${search_source}
67 |
`
68 | df.appendChild(div)
69 | div.onclick = () => {
70 | ha_cloud_music.showLoading()
71 | ha_cloud_music.fetchApi({ type: 'play_media', list: res, index }).finally(() => {
72 | ha_cloud_music.hideLoading()
73 | })
74 | }
75 | })
76 | $('.music-list').appendChild(df)
77 | }).finally(() => {
78 | ha_cloud_music.hideLoading()
79 | })
80 | }
81 | }
82 |
83 | // 定义DOM对象元素
84 | customElements.define('ha_cloud_music-search-musiclist', HaCloudMusicSearchMusicList);
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/local/card/ha_cloud_music-search.js:
--------------------------------------------------------------------------------
1 | class HaCloudMusicSearch extends HTMLElement {
2 |
3 | constructor() {
4 | super()
5 | this.created()
6 | }
7 | // 创建界面
8 | created() {
9 | /* ***************** 基础代码 ***************** */
10 | const shadow = this.attachShadow({ mode: 'open' });
11 | // 创建面板
12 | const ha_card = document.createElement('div');
13 | ha_card.className = 'ha_cloud_music-search'
14 | ha_card.innerHTML = `
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | `
28 | shadow.appendChild(ha_card)
29 | // 创建样式
30 | const style = document.createElement('style')
31 | style.textContent = `
32 | .ha_cloud_music-search{}
33 | .search-input{padding:10px;}
34 | .search-input input{width:100%; padding:5px;}
35 |
36 | .search-radio {display:flex; text-align: center; padding-bottom: 10px;}
37 | .search-radio label{width:100%;}
38 | `
39 | shadow.appendChild(style);
40 | // 保存核心DOM对象
41 | this.shadow = shadow
42 | this.$ = this.shadow.querySelector.bind(this.shadow)
43 | // 创建成功
44 | this.isCreated = true
45 |
46 | /* ***************** 附加代码 ***************** */
47 | let { $ } = this
48 | let txtSearchInput = $('.search-input input')
49 | txtSearchInput.onkeypress = (event) => {
50 | if (event.keyCode == 13) {
51 | searchAction()
52 | }
53 | }
54 | $('.search-radio').querySelectorAll("input[type='radio']").forEach(ele => {
55 | ele.onclick = () => {
56 | searchAction()
57 | }
58 | })
59 |
60 | const searchAction = () => {
61 | let value = txtSearchInput.value.trim()
62 | if (value) {
63 | txtSearchInput.value = ''
64 | let type = $(".search-radio input:checked").value
65 | ha_cloud_music.toast(`正在搜索【${value}】`)
66 | $('.search-list').innerHTML = ''
67 | ha_cloud_music.load(type == 'music' ? 'search-musiclist' : 'search-playlist').then(({ tagName }) => {
68 | const element = document.createElement(tagName)
69 | $('.search-list').appendChild(element)
70 | element.created(type, value)
71 | })
72 | }
73 | }
74 | }
75 | }
76 | customElements.define('ha_cloud_music-search', HaCloudMusicSearch);
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/local/card/ha_cloud_music-tabs.js:
--------------------------------------------------------------------------------
1 | class HaCloudMusicTabs extends HTMLElement {
2 |
3 | constructor() {
4 | super()
5 | this.created()
6 | }
7 | // 创建界面
8 | created() {
9 | /* ***************** 基础代码 ***************** */
10 | let arr = []
11 | let eleArr = []
12 | for (let ele of this.children) {
13 | const title = ele.dataset['title']
14 | arr.push(``)
15 | eleArr.push(ele)
16 | }
17 | // 创建面板
18 | const ha_card = document.createElement('mwc-tab-bar');
19 | ha_card.innerHTML = arr.join('')
20 | this.insertBefore(ha_card, this.children[0])
21 | // 创建样式
22 | const style = document.createElement('style')
23 | style.textContent = `
24 | .hide{display:none!important;}
25 | `
26 | this.appendChild(style);
27 | // 保存核心DOM对象
28 | this.$ = this.querySelector.bind(this)
29 | // 创建成功
30 | this.isCreated = true
31 |
32 | /* ***************** 附加代码 ***************** */
33 | const toggleTabs = (title) => {
34 | for (let i = 0; i < eleArr.length; i++) {
35 | let ele = eleArr[i]
36 | if (ele.dataset['title'] == title) {
37 | ele.classList.remove('hide')
38 | } else {
39 | ele.classList.add('hide')
40 | }
41 | }
42 | }
43 | let { $ } = this
44 | ha_card.addEventListener("MDCTab:interacted", (event) => {
45 | // console.log(event.detail.tabId)
46 | toggleTabs(event.detail.tabId)
47 | })
48 | toggleTabs('列表')
49 | }
50 | }
51 | customElements.define('ha_cloud_music-tabs', HaCloudMusicTabs);
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/local/card/ha_cloud_music-version.js:
--------------------------------------------------------------------------------
1 | class HaCloudMusicVersion extends HTMLElement {
2 |
3 | constructor() {
4 | super()
5 | this.created()
6 | }
7 | // 创建界面
8 | created() {
9 | /* ***************** 基础代码 ***************** */
10 | const shadow = this.attachShadow({ mode: 'open' });
11 | // 创建面板
12 | const ha_card = document.createElement('div');
13 | ha_card.className = 'ha_cloud_music-version'
14 | ha_card.innerHTML = `
15 |
16 |
17 |
插件版本:
18 |
19 |
20 | `
21 | shadow.appendChild(ha_card)
22 | // 创建样式
23 | const style = document.createElement('style')
24 | style.textContent = `
25 | .version-info{text-align:center;display:flex;padding:10px 0;margin-top:10px;}
26 | .version-info a{text-decoration:none;color:gray;width: 300px;}
27 | .version-info .line{border-bottom:1px solid #ccc; height:10px;width:60%;}
28 | `
29 | shadow.appendChild(style);
30 | // 保存核心DOM对象
31 | this.shadow = shadow
32 | this.$ = this.shadow.querySelector.bind(this.shadow)
33 | // 创建成功
34 | this.isCreated = true
35 |
36 | /* ***************** 附加代码 ***************** */
37 | let { $ } = this
38 | }
39 |
40 | updated(stateObj) {
41 | let { $ } = this
42 | $('.version').textContent = stateObj.attributes.version
43 | }
44 | }
45 | customElements.define('ha_cloud_music-version', HaCloudMusicVersion);
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/local/card/ha_cloud_music.js:
--------------------------------------------------------------------------------
1 | window.ha_cloud_music = {
2 | media_player: null,
3 | recorder: null,
4 | eventQueue: {},
5 | get hass() {
6 | return document.querySelector('home-assistant').hass
7 | },
8 | get entity_id() {
9 | return 'media_player.yun_yin_le'
10 | },
11 | get entity() {
12 | try {
13 | return this.hass.states[this.entity_id]
14 | } catch {
15 | return null
16 | }
17 | },
18 | get version() {
19 | return this.entity.attributes.version
20 | },
21 | fetchApi(params) {
22 | return this.hass.fetchWithAuth('/ha_cloud_music-api', {
23 | method: 'POST',
24 | body: JSON.stringify(params)
25 | }).then(res => res.json())
26 | },
27 | initAudio() {
28 | if (document.querySelector('#ha_cloud_music-recorder')) return;
29 | const script = document.createElement('script')
30 | script.id = 'ha_cloud_music-recorder'
31 | script.src = 'https://cdn.jsdelivr.net/gh/shaonianzhentan/lovelace-voice-speak@master/dist/recorder.mp3.min.js'
32 | script.onload = () => {
33 |
34 | }
35 | document.body.appendChild(script)
36 | },
37 | startRecording() {
38 | const recorder = Recorder({ type: "mp3", sampleRate: 16000 });
39 | recorder.open(function () {
40 | // 开始录音
41 | recorder.start();
42 | }, function (msg, isUserNotAllow) {
43 | // 用户拒绝未授权或不支持
44 | console.log((isUserNotAllow ? "UserNotAllow," : "") + "无法录音:" + msg);
45 | // 如果没有权限,则显示提示
46 | if (isUserNotAllow) {
47 | ha_cloud_music.toast('无法录音:' + msg)
48 | }
49 | });
50 | window.ha_cloud_music.recorder = recorder
51 | },
52 | stopRecording() {
53 | const { recorder, toast, hass } = window.ha_cloud_music
54 | recorder.stop(async (blob, duration) => {
55 | // 到达指定条件停止录音
56 | // console.log((window.URL || webkitURL).createObjectURL(blob), "时长:" + duration + "ms");
57 | recorder.close(); // 释放录音资源
58 | if (duration > 2000) {
59 | // 已经拿到blob文件对象想干嘛就干嘛:立即播放、上传
60 | let formData = new FormData()
61 | formData.append('mp3', blob)
62 | const res = await hass.fetchWithAuth('/ha_cloud_music-api', { method: 'PUT', body: formData }).then(res => res.json())
63 | toast(res.msg)
64 | } else {
65 | toast('当前录音时间没有2秒')
66 | }
67 | }, function (msg) {
68 | toast("录音失败:" + msg);
69 | });
70 | },
71 | callService(service_name, service_data = {}) {
72 | let arr = service_name.split('.')
73 | let domain = arr[0]
74 | let service = arr[1]
75 | this.hass.callService(domain, service, service_data)
76 | },
77 | // 媒体服务
78 | callMediaPlayerService(service_name, service_data = {}) {
79 | this.hass.callService('media_player', service_name, {
80 | entity_id: this.entity_id,
81 | ...service_data
82 | })
83 | },
84 | fire(type, data) {
85 | const event = new Event(type, {
86 | bubbles: true,
87 | cancelable: false,
88 | composed: true
89 | });
90 | event.detail = data;
91 | document.querySelector('home-assistant').dispatchEvent(event);
92 | },
93 | toast(message) {
94 | ha_cloud_music.fire("hass-notification", { message })
95 | },
96 | onmessage(type, data) {
97 | this.eventQueue[type](data)
98 | },
99 | addEventListener(type, func) {
100 | this.eventQueue[type] = func
101 | },
102 | async load(name) {
103 | if (Array.isArray(name)) {
104 | const arr = name.map(ele => {
105 | return this.load(ele)
106 | })
107 | return Promise.all(arr)
108 | }
109 | const tagName = `ha_cloud_music-${name}`
110 | const result = await import(`./${tagName}.js?ver=${this.version}`)
111 | return {
112 | tagName,
113 | result
114 | }
115 | }
116 | };
117 |
118 | (() => {
119 | const timer = setInterval(() => {
120 | if (!ha_cloud_music.entity) return
121 | clearInterval(timer)
122 | // 加载模块
123 | ha_cloud_music.load('player')
124 | ha_cloud_music.load('tabs').then(async () => {
125 | await ha_cloud_music.load(['playlist', 'lovelist', 'search', 'setting', 'voice', 'fmlist', 'version'])
126 | ha_cloud_music.load('panel')
127 | })
128 | }, 2000)
129 | })();
130 |
131 |
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/local/card/test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
40 |
41 |
42 |
43 |
44 |

45 |
46 |
测试 - 测试
47 |
—— QQ音乐
48 |
sdfsdfsdf
49 |
50 |
51 |
120 |
121 |
122 |
123 |
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "domain": "ha_cloud_music",
3 | "name": "\u4E91\u97F3\u4E50",
4 | "version": "1.0",
5 | "config_flow": true,
6 | "documentation": "https://github.com/shaonianzhentan/ha_cloud_music",
7 | "requirements": [
8 | "mutagen>=1.45.1",
9 | "python-mpd2>=3.0.5",
10 | "python-vlc>=1.1.2",
11 | "edge-tts>=4.0.3"
12 | ],
13 | "dependencies": [],
14 | "codeowners": ["@shaonianzhentan"],
15 | "iot_class": "cloud_polling"
16 | }
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/services.yaml:
--------------------------------------------------------------------------------
1 | load:
2 | description: 加载网易云音乐歌单音乐列表
3 | fields:
4 | id:
5 | name: 歌单ID
6 | description: 参数介绍【https://github.com/shaonianzhentan/ha_cloud_music/tree/master/custom_components/ha_cloud_music】
7 | example: '258244'
8 | selector:
9 | text:
10 | type:
11 | name: 音乐类型
12 | description: 网易歌单(playlist)、网易电台(djradio)、喜马拉雅专辑(ximalaya)
13 | example: 'ximalaya'
14 | selector:
15 | select:
16 | options:
17 | - playlist
18 | - djradio
19 | - ximalaya
20 | index:
21 | name: 要从第几首开始播放
22 | description: 要从第几首开始播放(如果超过列表总数量则从第1首开始播放)
23 | example: '1'
24 | selector:
25 | text:
26 | pick:
27 | description: 通过歌名点歌
28 | fields:
29 | name:
30 | name: 歌曲名称
31 | description: 歌曲名称
32 | example: '万有引力'
33 | selector:
34 | text:
35 | config:
36 | description: 配置修改(可单独设置)
37 | fields:
38 | is_voice:
39 | name: 启用语音识别
40 | description: (禁用/启用)语音识别(0:禁用,1:启用)
41 | example: 1
42 | selector:
43 | text:
44 | is_notify:
45 | name: 启用通知
46 | description: (禁用/启用)通知(0:禁用,1:启用)
47 | example: 1
48 | selector:
49 | text:
50 | play_mode:
51 | name: 播放模式
52 | description: 播放模式(0:列表循环,1:顺序播放,2:随机播放,3:单曲循环)
53 | example: 1
54 | selector:
55 | number:
56 | min: 0
57 | max: 4
58 | step: 1
59 | mode: slider
60 | media_rate:
61 | name: 播放速度
62 | description: 注意:播放速度只支持内置VLC(1为正常速度)
63 | example: 1
64 | selector:
65 | number:
66 | min: 1
67 | max: 3
68 | step: 0.5
69 | mode: slider
70 | tts_mode:
71 | name: TTS声音模式
72 | description: TTS声音模式(1:标准男声,2:标准女声,3:情感男声,4:情感女声)
73 | example: 4
74 | selector:
75 | number:
76 | min: 1
77 | max: 4
78 | step: 1
79 | mode: slider
80 | tts_volume:
81 | name: TTS音量
82 | description: TTS音量(1到100)
83 | example: 50
84 | selector:
85 | number:
86 | min: 1
87 | max: 100
88 | step: 1
89 | mode: slider
90 | tts_before_message:
91 | name: tts服务前置消息
92 | description: tts服务前置消息
93 | example: 主人:
94 | selector:
95 | text:
96 | tts_after_message:
97 | name: tts服务后置消息
98 | description: tts服务后置消息
99 | example: 。我是爱你的小喵
100 | selector:
101 | text:
102 | tts:
103 | description: 文字转语音
104 | fields:
105 | message:
106 | name: 要播放的文字
107 | description: 要播放的文字(支持内置模板格式)
108 | example: '现在的时间是{{now().strftime("%H:%M")}}'
109 | selector:
110 | text:
111 | cache:
112 | description: 缓存音乐文件
113 | fields:
114 | name:
115 | name: mp3文件名称
116 | description: mp3文件名称
117 | example: '不会用就别乱调用,可能会把系统搞卡死'
118 | selector:
119 | text:
120 | url:
121 | name: mp3音乐链接
122 | description: mp3音乐链接
123 | example: '不会用千万别乱调用,可能会把系统搞卡死崩溃'
124 | selector:
125 | text:
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/shaonianzhentan.py:
--------------------------------------------------------------------------------
1 | import asyncio, aiohttp, json
2 | from urllib.parse import urlparse
3 |
4 | # 获取HTTP内容
5 | async def fetch_text(url, headers = {}):
6 | p = urlparse(url)
7 | HEADERS = {
8 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36',
9 | 'Referer': f'{p.scheme}//{p.netloc}'
10 | }
11 | HEADERS.update(headers)
12 | text = None
13 | connector = aiohttp.TCPConnector(verify_ssl=False)
14 | async with aiohttp.ClientSession(headers=HEADERS, connector=connector) as session:
15 | async with session.get(url) as resp:
16 | text = await resp.text()
17 | return text
18 |
19 | # 获取HTTP内容JSON格式
20 | async def fetch_json(url, headers = {}):
21 | text = await fetch_text(url, headers)
22 | result = {}
23 | if text is not None:
24 | result = json.loads(text)
25 | return result
26 |
27 | # 获取HTTP请求信息
28 | async def fetch_info(url):
29 | p = urlparse(url)
30 | headers = {
31 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36',
32 | 'Referer': f'{p.scheme}//{p.netloc}'
33 | }
34 | connector = aiohttp.TCPConnector(verify_ssl=False)
35 | async with aiohttp.ClientSession(headers=headers, connector=connector) as session:
36 | async with session.get(url) as response:
37 | return {
38 | 'status': response.status,
39 | 'url': str(response.url)
40 | }
41 |
42 | # 执行异步方法
43 | def async_create_task(async_func):
44 | loop = asyncio.get_event_loop()
45 | loop.run_until_complete(async_func)
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/source_other.py:
--------------------------------------------------------------------------------
1 | import time, datetime
2 | import threading
3 |
4 | class MediaPlayerOther():
5 |
6 | # 初始化
7 | def __init__(self, entity_id, media=None):
8 | # 播放器相同字段
9 | self.entity_id = entity_id
10 | self._media = media
11 | self._muted = False
12 | self.rate = 1
13 | self.media_position = 0
14 | self.media_duration = 0
15 | self.media_position_updated_at = datetime.datetime.now()
16 | self.state = 'idle'
17 | self.is_tts = False
18 | self.is_on = True
19 | # 定时更新
20 | self.count = 0
21 | self.volume_level = 1
22 | self.timer = threading.Timer(1, self.update)
23 | self.timer.start()
24 |
25 | def update(self):
26 | # 更新
27 | try:
28 | hass = self._media._hass
29 | # 读取当前实体信息
30 | entity = hass.states.get(self.entity_id)
31 | attr = entity.attributes
32 | if 'media_position' in attr:
33 | media_position = attr['media_position']
34 | # 如果进度是字符串,并且包含冒号
35 | if isinstance(media_position, str) and ':' in media_position:
36 | arr = media_position.split(':')
37 | media_position = int(arr[0])
38 | media_duration = int(arr[1])
39 | else:
40 | media_duration = attr['media_duration']
41 | # print("当前进度:%s,总时长:%s"%(media_position, media_duration))
42 | # 判断是否下一曲
43 | if media_duration > 0:
44 | if media_duration - media_position <= 3:
45 | print('执行下一曲方法')
46 | if self._media is not None and self.state == 'playing' and self.is_tts == False and self.is_on == True and self.count > 0:
47 | self.count = -5
48 | self.state = 'idle'
49 | self._media.media_end_next()
50 | # 最后10秒时,实时更新
51 | elif media_duration - media_position < 10:
52 | print("当前进度:%s,总时长:%s"%(media_position, media_duration))
53 | hass.async_create_task(hass.services.async_call('homeassistant', 'update_entity', {'entity_id': self.entity_id}))
54 |
55 | # 防止通信太慢,导致进度跟不上自动下下一曲
56 | self.count = self.count + 1
57 | if self.count > 100:
58 | self.count = 0
59 |
60 | # 正常获取值
61 | self.media_position = media_position
62 | self.media_duration = media_duration
63 | self.volume_level = attr['volume_level']
64 | self.media_position_updated_at = datetime.datetime.now()
65 | except Exception as e:
66 | print('出现异常', e)
67 | # 递归调用自己
68 | self.timer = threading.Timer(2, self.update)
69 | self.timer.start()
70 |
71 | def reloadURL(self, url, position):
72 | # 重新加载URL
73 | self.load(url)
74 | time.sleep(1)
75 | # 先把声音设置为0,然后调整位置之后再还原
76 | self.set_volume_level(0)
77 | time.sleep(1)
78 | self.seek(position)
79 | time.sleep(1)
80 | self.set_volume_level(self._media.volume_level)
81 |
82 | def load(self, url):
83 | # 加载URL
84 | url = url.replace("https://", "http://")
85 | self.call_service('play_media', {
86 | 'media_content_id': url,
87 | 'media_content_type': 'music'
88 | })
89 | # 不是TTS时才设置状态
90 | if self.is_tts == False:
91 | self.state = 'playing'
92 |
93 | def play(self):
94 | # 播放
95 | self.state = 'playing'
96 | self.call_service('media_play', {})
97 |
98 | def pause(self):
99 | # 暂停
100 | self.state = 'paused'
101 | self.call_service('media_pause', {})
102 |
103 | def seek(self, position):
104 | # 设置进度
105 | self.call_service('media_seek', {'seek_position': position})
106 |
107 | def mute_volume(self, mute):
108 | # 静音
109 | self.call_service('volume_mute', {'is_volume_muted': mute})
110 |
111 | def set_volume_level(self, volume):
112 | # 设置音量
113 | self.call_service('volume_set', {'volume_level': volume})
114 |
115 | def volume_up(self):
116 | # 增加音量
117 | self.call_service('volume_up', {})
118 |
119 | def volume_down(self):
120 | # 减少音量
121 | self.call_service('volume_down', {})
122 |
123 | def stop(self):
124 | # 停止
125 | self.pause()
126 | self.timer.cancel()
127 |
128 | def set_rate(self, rate):
129 | # 设置播放速度
130 | return 1
131 |
132 | def log(self, msg):
133 | if self._media is not None:
134 | self._media.log(msg, 'source_other')
135 |
136 | def call_service(self, service, data):
137 | hass = self._media._hass
138 | data.update({'entity_id': self.entity_id})
139 | hass.async_create_task(hass.services.async_call('media_player', service, data))
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/source_vlc.py:
--------------------------------------------------------------------------------
1 | # VLC播放器
2 | import time, datetime
3 |
4 | class MediaPlayerVLC():
5 |
6 | # 初始化
7 | def __init__(self, config, media=None):
8 | # 播放器相同字段
9 | self.config = config
10 | self._media = media
11 | self._muted = False
12 | self.media_position = 0
13 | self.media_duration = 0
14 | self.media_position_updated_at = datetime.datetime.now()
15 | self.state = 'idle'
16 | self.is_tts = False
17 | self.is_on = True
18 |
19 | try:
20 | import vlc
21 | self._instance = vlc.Instance()
22 | self._client = self._instance.media_player_new()
23 | _event_manager = self._client.event_manager()
24 | _event_manager.event_attach(vlc.EventType.MediaPlayerEndReached, self.end)
25 | _event_manager.event_attach(vlc.EventType.MediaPlayerPositionChanged, self.update)
26 | self.is_support = True
27 | except Exception as e:
28 | print(e)
29 | self.is_support = False
30 |
31 | @property
32 | def volume_level(self):
33 | return self._client.audio_get_volume() / 100
34 |
35 | @property
36 | def rate(self):
37 | return round(self._client.get_rate(), 1)
38 |
39 | def end(self, event):
40 | # 音乐结束
41 | if self._media is not None and self.is_tts == False and self.is_on == True:
42 | print('执行下一曲')
43 | self._media.media_end_next()
44 |
45 | def update(self, event):
46 | # 如果是TTS中,则不更新进度
47 | if self.is_tts == False:
48 | media_duration = int(self._client.get_length() / 1000)
49 | media_position = int(self._client.get_position() * media_duration)
50 | # print("当前进度:%s,总时长:%s"%(media_position, media_duration))
51 | self.media_position = media_position
52 | self.media_duration = media_duration
53 | self.media_position_updated_at = datetime.datetime.now()
54 | self._muted = (self._client.audio_get_mute() == 1)
55 |
56 | def reloadURL(self, url, position):
57 | # 重新加载URL
58 | self.load(url)
59 | # 先把声音设置为0,然后调整位置之后再还原
60 | self.set_volume_level(0)
61 | # 局域网资源,则优化快进规则
62 | if self._media.base_url in url:
63 | time.sleep(0.1)
64 | self.seek(position)
65 | else:
66 | time.sleep(1)
67 | self.seek(position)
68 | time.sleep(1)
69 | self.set_volume_level(self._media.volume_level)
70 |
71 | def load(self, url):
72 | # 加载URL
73 | url = url.replace("https://", "http://")
74 | self._client.set_media(self._instance.media_new(url))
75 | self._client.play()
76 | # 不是TTS时才设置状态
77 | if self.is_tts == False:
78 | self.state = 'playing'
79 |
80 | def play(self):
81 | self.state = 'playing'
82 | # 播放
83 | if self._client.is_playing() == False:
84 | self._client.play()
85 |
86 | def pause(self):
87 | self.state = 'paused'
88 | # 暂停
89 | if self._client.is_playing() == True:
90 | self._client.pause()
91 |
92 | def seek(self, position):
93 | # 设置进度
94 | track_length = self._client.get_length() / 1000
95 | if track_length > 0:
96 | self.media_position = position
97 | self._client.set_position(position / track_length)
98 |
99 | def mute_volume(self, mute):
100 | # 静音
101 | self._client.audio_set_mute(mute)
102 | self._muted = mute
103 |
104 | def set_volume_level(self, volume):
105 | # 设置音量
106 | self._client.audio_set_volume(int(volume * 100))
107 |
108 | def volume_up(self):
109 | # 增加音量
110 | current_volume = self._client.audio_get_volume()
111 | if current_volume <= 100:
112 | self._client.audio_set_volume(current_volume + 5)
113 |
114 | def volume_down(self):
115 | # 减少音量
116 | current_volume = self._client.audio_get_volume()
117 | if current_volume <= 100:
118 | self._client.audio_set_volume(current_volume - 5)
119 |
120 | def stop(self):
121 | # 停止
122 | self._client.release()
123 | self._instance.release()
124 |
125 | def set_rate(self, rate):
126 | # 设置播放速度
127 | return self._client.set_rate(rate)
128 |
129 | '''
130 | mm = MediaPlayerVLC({})
131 | if mm.is_support:
132 | mm.load('https://m701.music.126.net/20201225165051/fd7f6db013996a3316a31bce123d9399/jdymusic/obj/wo3DlMOGwrbDjj7DisKw/5258591663/61bf/33cf/e6a5/da47602351f7f71aea8c1e88de587411.mp3')
133 | mm.set_rate(1)
134 |
135 | while True:
136 | pass
137 | '''
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/source_web.py:
--------------------------------------------------------------------------------
1 | # 网页播放器
2 | import time, datetime
3 | from homeassistant.components import websocket_api
4 | import voluptuous as vol
5 |
6 | WS_TYPE_MEDIA_PLAYER = "ha_cloud_music_event"
7 | SCHEMA_WEBSOCKET = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
8 | {
9 | "type": WS_TYPE_MEDIA_PLAYER,
10 | vol.Optional("data"): dict,
11 | }
12 | )
13 |
14 | class MediaPlayerWEB():
15 |
16 | # 初始化
17 | def __init__(self, config, media=None):
18 | # 播放器相同字段
19 | self.config = config
20 | self._media = media
21 | self._muted = False
22 | self.rate = 1
23 | self.media_position = 0
24 | self.media_duration = 0
25 | self.media_position_updated_at = datetime.datetime.now()
26 | self.state = 'idle'
27 | self.is_tts = False
28 | self.is_on = True
29 | # 不同字段
30 | self.count = 0
31 | self.volume_level = 1
32 | self.is_support = True
33 | # 监听web播放器的更新
34 | if media is not None:
35 | self.hass = media._hass
36 | # 监听web播放器的更新
37 | self.hass.components.websocket_api.async_register_command(
38 | WS_TYPE_MEDIA_PLAYER,
39 | self.update,
40 | SCHEMA_WEBSOCKET
41 | )
42 |
43 | def update(self, hass, connection, msg):
44 | data = msg['data']
45 | if self._media is not None:
46 | # 消息类型
47 | type = data.get('type', '')
48 | # 客户端ID
49 | client_id = data.get('client_id', '')
50 | if type == 'init':
51 | # 初始化连接成功
52 | self.hass.bus.fire("ha_cloud_music_event", {"type": "init", "data": {
53 | 'client_id': client_id,
54 | 'volume_level': self.volume_level,
55 | 'media_url': self._media.media_url,
56 | 'media_position': self.media_position
57 | }})
58 | elif type == 'update':
59 | media_position = data.get('media_position', 0)
60 | media_duration = data.get('media_duration', 0)
61 | # print(self.media_position, self.media_duration)
62 | # 更新进度
63 | if self.media_duration is not None and self.media_position is not None \
64 | and self.media_duration > 0 and self.media_position > 0 \
65 | and self.media_position + 3 >= self.media_duration \
66 | and self.count > 0:
67 | print('执行下一曲')
68 | self.count = -10
69 | self._media.media_end_next()
70 | # 防止通信太慢,导致进度跟不上自动下下一曲
71 | self.count = self.count + 1
72 | if self.count > 100:
73 | self.count = 0
74 | # 更新数据
75 | self.volume_level = data.get('volume_level')
76 | self._muted = data.get('is_volume_muted')
77 | self.media_duration = media_duration
78 | self.media_position = media_position
79 | self.media_position_updated_at = datetime.datetime.now()
80 | # 回调结果
81 | # self.connection = connection
82 |
83 | def reloadURL(self, url, position):
84 | # 重新加载URL
85 | self.load(url)
86 | # 先把声音设置为0,然后调整位置之后再还原
87 | self.set_volume_level(0)
88 | # 局域网资源,则优化快进规则
89 | if self._media.base_url in url:
90 | time.sleep(0.1)
91 | self.seek(position)
92 | else:
93 | time.sleep(1)
94 | self.seek(position)
95 | time.sleep(1)
96 | self.set_volume_level(self._media.volume_level)
97 |
98 | def load(self, url):
99 | # 使用TTS服务
100 | if self.is_tts:
101 | self.hass.bus.fire("ha_cloud_music_event", {"type": "tts", "data": url})
102 | else:
103 | # 加载URL
104 | self.hass.bus.fire("ha_cloud_music_event", {"type": "load", "data": url})
105 | self.state = 'playing'
106 |
107 | def play(self):
108 | # 播放
109 | self.hass.bus.fire("ha_cloud_music_event", {"type": "play"})
110 | self.state = "playing"
111 |
112 | def pause(self):
113 | # 暂停
114 | self.hass.bus.fire("ha_cloud_music_event", {"type": "pause"})
115 | self.state = "paused"
116 |
117 | def seek(self, position):
118 | # 设置进度
119 | self.hass.bus.fire("ha_cloud_music_event", {"type": "media_position", "data": position})
120 |
121 | def mute_volume(self, mute):
122 | # 静音
123 | self.hass.bus.fire("ha_cloud_music_event", {"type": "is_volume_muted", "data": mute})
124 |
125 | def set_volume_level(self, volume):
126 | # 设置音量
127 | self.hass.bus.fire("ha_cloud_music_event", {"type": "volume_set", "data": volume})
128 |
129 | def volume_up(self):
130 | # 增加音量
131 | current_volume = self.volume_level
132 | if current_volume <= 100:
133 | self.set_volume_level(current_volume + 5)
134 |
135 | def volume_down(self):
136 | # 减少音量
137 | current_volume = self.volume_level
138 | if current_volume <= 100:
139 | self.set_volume_level(current_volume - 5)
140 |
141 | def stop(self):
142 | # 停止
143 | self.hass.bus.fire("ha_cloud_music_event", {"type": "pause"})
144 |
145 | def set_rate(self, rate):
146 | # 设置播放速度
147 | return 1
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/source_windows.py:
--------------------------------------------------------------------------------
1 | # Windows应用
2 | import time, datetime
3 | from homeassistant.components import websocket_api
4 | import voluptuous as vol
5 |
6 | WS_TYPE_MEDIA_PLAYER = "ha_windows_updated"
7 | SCHEMA_WEBSOCKET = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend(
8 | {
9 | "type": WS_TYPE_MEDIA_PLAYER,
10 | vol.Optional("data"): dict,
11 | }
12 | )
13 |
14 | class MediaPlayerWindows():
15 |
16 | # 初始化
17 | def __init__(self, config, media=None):
18 | # 播放器相同字段
19 | self.config = config
20 | self._media = media
21 | self._muted = False
22 | self.rate = 1
23 | self.media_position = 0
24 | self.media_duration = 0
25 | self.media_position_updated_at = datetime.datetime.now()
26 | self.state = 'idle'
27 | self.is_tts = False
28 | self.is_on = True
29 | # 不同字段
30 | self.volume_level = 1
31 | self.is_support = True
32 | # 监听web播放器的更新
33 | if media is not None:
34 | self.hass = media._hass
35 | # 监听web播放器的更新
36 | print(WS_TYPE_MEDIA_PLAYER)
37 | self.hass.components.websocket_api.async_register_command(
38 | WS_TYPE_MEDIA_PLAYER,
39 | self.update,
40 | SCHEMA_WEBSOCKET
41 | )
42 |
43 | def update(self, hass, connection, msg):
44 | if self._media is not None:
45 | data = msg['data']
46 | # print(data)
47 | # 消息类型
48 | _type = data.get('type')
49 | if _type == 'music_info':
50 | self._muted = data.get('is_volume_muted', False)
51 | self._media._volume_level = data.get('volume_level', 1)
52 | self.media_position = data.get('media_position', 0)
53 | self.media_duration = data.get('media_duration', 0)
54 | self.media_position_updated_at = datetime.datetime.now()
55 | elif _type == 'music_end':
56 | self._media.media_end_next()
57 | elif _type == 'music_state':
58 | self.state = data.get('state')
59 |
60 | def reloadURL(self, url, position):
61 | # 重新加载URL
62 | self.load(url)
63 | # 先把声音设置为0,然后调整位置之后再还原
64 | volume_level = self._media.volume_level
65 | self.set_volume_level(0)
66 | # 局域网资源,则优化快进规则
67 | if self._media.base_url in url:
68 | time.sleep(0.1)
69 | self.seek(position)
70 | else:
71 | time.sleep(1)
72 | self.seek(position)
73 | time.sleep(1)
74 | # 如果重置是为0,则恢复正常
75 | if volume_level == 0:
76 | volume_level = 1
77 | self.set_volume_level(volume_level)
78 |
79 | def load(self, url):
80 | # 使用TTS服务
81 | if self.is_tts:
82 | self.fire_event({"type": "tts", "url": url})
83 | else:
84 | # 加载URL
85 | self.fire_event({"type": "load", "url": url})
86 | self.state = 'playing'
87 |
88 | def play(self):
89 | # 播放
90 | self.fire_event({"type": "play"})
91 | self.state = "playing"
92 |
93 | def pause(self):
94 | # 暂停
95 | self.fire_event({"type": "pause"})
96 | self.state = "paused"
97 |
98 | def seek(self, position):
99 | # 设置进度
100 | self.fire_event({"type": "media_position", "position": position})
101 |
102 | def mute_volume(self, mute):
103 | # 静音
104 | self.fire_event({"type": "is_volume_muted", "mute": mute})
105 |
106 | def set_volume_level(self, volume):
107 | # 设置音量
108 | self.fire_event({"type": "volume_set", "volume": volume})
109 |
110 | def volume_up(self):
111 | # 增加音量
112 | current_volume = self.volume_level
113 | if current_volume < 1:
114 | self.set_volume_level(current_volume + 0.1)
115 |
116 | def volume_down(self):
117 | # 减少音量
118 | current_volume = self.volume_level
119 | if current_volume > 0:
120 | self.set_volume_level(current_volume - 0.1)
121 |
122 | def stop(self):
123 | # 停止
124 | self.fire_event({"type": "pause"})
125 |
126 | def set_rate(self, rate):
127 | # 设置播放速度
128 | return 1
129 |
130 | def fire_event(self, data):
131 | self.hass.bus.fire("ha_windows", { 'type': 'music', 'music': data})
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/test.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import tempfile, shutil
3 | import edgeTTS
4 |
5 | async def main():
6 | communicate = edgeTTS.Communicate()
7 | lang = 'zh-CN'
8 | voice = 'zh-CN-XiaoxiaoNeural'
9 | message = '测试一下'
10 | xml = '' \
14 | f'' \
15 | f'{message}'
16 | with open('test.mp3', 'wb') as fp:
17 | async for i in communicate.run(xml, customspeak=True):
18 | if i[2] is not None:
19 | fp.write(i[2])
20 | print(fp.name)
21 |
22 | if __name__ == "__main__":
23 | asyncio.run(main())
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/translations/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "config": {
3 | "step": {
4 | "user": {
5 | "title": "云音乐",
6 | "description": "项目: https://github.com/shaonianzhentan/ha_cloud_music",
7 | "data": {
8 | "api_url": "网易云音乐接口地址",
9 | "mpd_host": "MPD播放器host",
10 | "is_voice": "启用语音控制"
11 | }
12 | }
13 | },
14 | "error": {},
15 | "abort": {
16 | "single_instance_allowed": "仅允许单个配置."
17 | }
18 | },
19 | "options": {
20 | "step": {
21 | "user": {
22 | "title": "云音乐",
23 | "description": "配置",
24 | "data": {
25 | "find_api_url": "全网音乐查找接口",
26 | "user": "网易云音乐的账号",
27 | "password": "网易云音乐的密码",
28 | "tts_before_message": "TTS前置消息",
29 | "tts_after_message": "TTS后置消息",
30 | "tts_mode": "TTS声音模式",
31 | "is_notify": "启用通知消息"
32 | }
33 | }
34 | },
35 | "error": {}
36 | }
37 | }
--------------------------------------------------------------------------------
/custom_components/ha_cloud_music/util.py:
--------------------------------------------------------------------------------
1 | """Util for Conversation."""
2 | import re
3 | import string
4 | import random
5 |
6 | ########################################## 常量
7 |
8 | ########################################## 去掉前后标点符号
9 | def trim_char(text):
10 | return text.strip(' 。,、':∶;?‘’“”〝〞ˆˇ﹕︰﹔﹖﹑·¨….¸;!´?!~—ˉ|‖"〃`@﹫¡¿﹏﹋﹌︴々﹟#﹩$﹠&﹪%*﹡﹢﹦﹤‐ ̄¯―﹨ˆ˜﹍﹎+=<__-\ˇ~﹉﹊()〈〉‹›﹛﹜『』〖〗[]《》〔〕{}「」【】︵︷︿︹︽_﹁﹃︻︶︸﹀︺︾ˉ﹂﹄︼')
11 |
12 | ########################################## 汉字转数字
13 | common_used_numerals_tmp ={'零':0, '一':1, '二':2, '两':2, '三':3, '四':4, '五':5, '六':6, '七':7, '八':8, '九':9, '十':10, '百':100, '千':1000, '万':10000, '亿':100000000}
14 | common_used_numerals = {}
15 | for key in common_used_numerals_tmp:
16 | common_used_numerals[key.encode('cp936').decode('cp936')] = common_used_numerals_tmp[key]
17 |
18 | def chinese2digits(uchars_chinese):
19 | try:
20 | uchars_chinese = uchars_chinese.encode('cp936').decode('cp936')
21 | total = 0
22 | r = 1 #表示单位:个十百千...
23 | for i in range(len(uchars_chinese) - 1, -1, -1):
24 | val = common_used_numerals.get(uchars_chinese[i])
25 | if val >= 10 and i == 0: #应对 十三 十四 十*之类
26 | if val > r:
27 | r = val
28 | total = total + val
29 | else:
30 | r = r * val
31 | #total =total + r * x
32 | elif val >= 10:
33 | if val > r:
34 | r = val
35 | else:
36 | r = r * val
37 | else:
38 | total = total + r * val
39 | return total
40 | except Exception as ex:
41 | return None
42 |
43 | # 判断是否数字
44 | def is_number(s):
45 | try:
46 | float(s)
47 | return True
48 | except ValueError:
49 | pass
50 |
51 | try:
52 | import unicodedata
53 | unicodedata.numeric(s)
54 | return True
55 | except (TypeError, ValueError):
56 | pass
57 |
58 | return False
59 |
60 | def format_number(num):
61 | if is_number(num) == False:
62 | num = chinese2digits(num)
63 | return float(num)
64 |
65 | ########################################## (我想听|播放)(.+)的(歌|音乐)
66 | def matcher_singer_music(text):
67 | matchObj = re.match(r'(我想听|播放)(.+)的(歌|音乐)', text)
68 | if matchObj is not None:
69 | return matchObj.group(2)
70 |
71 | ########################################## 播放(电台|歌单|歌曲|新闻|广播|专辑)(.*)
72 | def matcher_play_music(text):
73 | matchObj = re.match(r'播放(电台|歌单|歌曲|新闻|广播|专辑)(.*)', text)
74 | if matchObj is not None:
75 | return (matchObj.group(1), matchObj.group(2))
76 |
77 | ########################################## (播放|暂停)音乐
78 | def matcher_play_pause(text):
79 | matchObj = re.match(r'(播放|暂停)音乐', text)
80 | if matchObj is not None:
81 | return matchObj.group(1)
82 |
83 | ########################################## ((播放)*)(上|下|前|后)一(曲|首)
84 | def matcher_prev_next(text):
85 | matchObj = re.match(r'((播放)*)(上|下|前|后)一(曲|首)', text)
86 | if matchObj is not None:
87 | return matchObj.group(3)
88 |
89 | ########################################## 集数调整
90 | def matcher_playlist_index(text):
91 | matchObj = re.match(r'播放第(.+)(集|首)(.*)', text)
92 | if matchObj is not None:
93 | return format_number(matchObj.group(1))
94 |
95 | ########################################## 音量调整
96 | def matcher_volume_setting(text):
97 | matchObj = re.match(r'(把tts|把音乐|音乐|tts)(声音|音量)调到(.+)', text)
98 | if matchObj is not None:
99 | volume_level = matchObj.group(3)
100 | if volume_level == '最大':
101 | volume_level = 100.0
102 | elif volume_level == '最小':
103 | volume_level = 20.0
104 | else:
105 | volume_level = format_number(volume_level)
106 | return (matchObj.group(1), float(volume_level) / 100.0)
--------------------------------------------------------------------------------
/hacs.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "云音乐",
3 | "country": "CN",
4 | "render_readme": true,
5 | "domains": ["media_player"]
6 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-mmplayer",
3 | "version": "1.8.0",
4 | "private": true,
5 | "description": "Online music player",
6 | "author": "maomao1996 <1714487678@qq.com>",
7 | "bugs": {
8 | "url": "https://github.com/maomao1996/Vue-mmPlayer/issues"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "https://github.com/maomao1996/Vue-mmPlayer"
13 | },
14 | "license": "MIT",
15 | "scripts": {
16 | "serve": "vue-cli-service serve",
17 | "build": "vue-cli-service build",
18 | "lint": "vue-cli-service lint"
19 | },
20 | "dependencies": {
21 | "axios": "^0.19.0",
22 | "core-js": "^3.4.1",
23 | "fastclick": "^1.0.6",
24 | "vue": "^2.6.10",
25 | "vue-lazyload": "^1.3.3",
26 | "vue-router": "^3.1.3",
27 | "vuex": "^3.1.2"
28 | },
29 | "devDependencies": {
30 | "@vue/cli-plugin-babel": "^4.0.5",
31 | "@vue/cli-plugin-eslint": "^4.0.5",
32 | "@vue/cli-service": "^4.0.5",
33 | "@vue/eslint-config-standard": "^4.0.0",
34 | "babel-eslint": "^10.0.3",
35 | "dayjs": "^1.8.24",
36 | "eslint": "^6.6.0",
37 | "eslint-plugin-vue": "^6.0.1",
38 | "less": "^3.10.3",
39 | "less-loader": "^5.0.0",
40 | "style-resources-loader": "^1.3.2",
41 | "vue-cli-plugin-style-resources-loader": "^0.1.3",
42 | "vue-template-compiler": "^2.6.10"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | autoprefixer: {}
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shaonianzhentan/cloud_music/637f97185783e5dece10d83899489653c7edd6d2/public/favicon.ico
--------------------------------------------------------------------------------
/public/img/warn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shaonianzhentan/cloud_music/637f97185783e5dece10d83899489653c7edd6d2/public/img/warn.png
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 | mmPlayer 在线音乐播放器
12 |
16 |
17 |
21 |
125 |
133 | <% if ( NODE_ENV === 'production' ) { %>
134 |
144 | <% } %>
145 |
146 |
147 |
150 |
153 |
154 |
155 |
156 |
157 |
--------------------------------------------------------------------------------
/public/prompt.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | mmPlayer | 温馨提示
7 |
16 |
17 |
18 |
19 |
20 |
21 |  |
22 |
23 |
24 | |
25 | 很抱歉!为了更好的体验,本站限制以下浏览器访问: |
26 | IE浏览器和使用IE内核的浏览器 |
27 | 解决办法:下载其他主流浏览器或者切换浏览器内核为极速内核 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
13 |
14 |
15 |
16 |
17 |
18 |
118 |
119 |
137 |
--------------------------------------------------------------------------------
/src/api/index.js:
--------------------------------------------------------------------------------
1 | import qs from 'qs'
2 | import axios from '@/utils/axios'
3 | import { defaultLimit } from '@/config'
4 | import { formatTopSongs } from '@/utils/song'
5 |
6 | axios.defaults.baseURL = 'https://api.mtnhao.com'
7 |
8 | async function proxyGet(url, data) {
9 | const ha = top.document.querySelector('home-assistant')
10 | if (!ha) {
11 | return axios.get(url, data)
12 | }
13 | if (data && 'params' in data) {
14 | url = `${url}?${qs.stringify(data.params)}`
15 | }
16 | // console.log(url)
17 | return ha.hass.fetchWithAuth('/ha_cloud_music-api', {
18 | method: 'POST',
19 | body: JSON.stringify({
20 | type: 'web',
21 | url
22 | })
23 | }).then(res => res.json())
24 | }
25 |
26 | // 排行榜列表
27 | export function getToplistDetail() {
28 | return proxyGet('/toplist/detail')
29 | }
30 |
31 | // 推荐歌单
32 | export function getPersonalized() {
33 | return proxyGet('/personalized')
34 | }
35 |
36 | // 歌单详情
37 | export function getPlaylistDetail(id) {
38 | return new Promise((resolve, reject) => {
39 | proxyGet('/playlist/detail', {
40 | params: { id }
41 | })
42 | .then(({ playlist }) => playlist)
43 | .then(playlist => {
44 | const { trackIds, tracks } = playlist
45 | // 过滤完整歌单 如排行榜
46 | if (tracks.length === trackIds.length) {
47 | playlist.tracks = formatTopSongs(playlist.tracks)
48 | resolve(playlist)
49 | return
50 | }
51 | // 限制歌单详情最大 500
52 | const ids = trackIds
53 | .slice(0, 500)
54 | .map(v => v.id)
55 | .toString()
56 | getMusicDetail(ids).then(({ songs }) => {
57 | playlist.tracks = formatTopSongs(songs)
58 | resolve(playlist)
59 | })
60 | })
61 | })
62 | }
63 |
64 | // 搜索
65 | export function search(keywords, page = 0, limit = defaultLimit) {
66 | return proxyGet('/search', {
67 | params: {
68 | offset: page * limit,
69 | limit: limit,
70 | keywords
71 | }
72 | })
73 | }
74 |
75 | // 热搜
76 | export function searchHot() {
77 | return proxyGet('/search/hot')
78 | }
79 |
80 | // 获取用户歌单详情
81 | export function getUserPlaylist(uid) {
82 | return proxyGet('/user/playlist', {
83 | params: {
84 | uid
85 | }
86 | })
87 | }
88 |
89 | // 获取歌曲详情
90 | export function getMusicDetail(ids) {
91 | return proxyGet('/song/detail', {
92 | params: {
93 | ids
94 | }
95 | })
96 | }
97 |
98 | // 获取音乐是否可以用
99 | export function getCheckMusic(id) {
100 | return proxyGet('/check/music', {
101 | params: {
102 | id
103 | }
104 | })
105 | }
106 |
107 | // 获取音乐地址
108 | export function getMusicUrl(id) {
109 | return proxyGet('/song/url', {
110 | params: {
111 | id
112 | }
113 | })
114 | }
115 |
116 | // 获取歌词
117 | export function getLyric(id) {
118 | const url = '/lyric'
119 | return proxyGet(url, {
120 | params: {
121 | id
122 | }
123 | })
124 | }
125 |
126 | // 获取音乐评论
127 | export function getComment(id, page, limit = defaultLimit) {
128 | return proxyGet('/comment/music', {
129 | params: {
130 | offset: page * limit,
131 | limit: limit,
132 | id
133 | }
134 | })
135 | }
136 |
--------------------------------------------------------------------------------
/src/assets/background/bg-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shaonianzhentan/cloud_music/637f97185783e5dece10d83899489653c7edd6d2/src/assets/background/bg-1.jpg
--------------------------------------------------------------------------------
/src/assets/background/bg-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shaonianzhentan/cloud_music/637f97185783e5dece10d83899489653c7edd6d2/src/assets/background/bg-2.jpg
--------------------------------------------------------------------------------
/src/assets/img/album_cover_player.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shaonianzhentan/cloud_music/637f97185783e5dece10d83899489653c7edd6d2/src/assets/img/album_cover_player.png
--------------------------------------------------------------------------------
/src/assets/img/default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shaonianzhentan/cloud_music/637f97185783e5dece10d83899489653c7edd6d2/src/assets/img/default.png
--------------------------------------------------------------------------------
/src/assets/img/player_cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shaonianzhentan/cloud_music/637f97185783e5dece10d83899489653c7edd6d2/src/assets/img/player_cover.png
--------------------------------------------------------------------------------
/src/assets/img/wave.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shaonianzhentan/cloud_music/637f97185783e5dece10d83899489653c7edd6d2/src/assets/img/wave.gif
--------------------------------------------------------------------------------
/src/base/mm-icon/mm-icon.vue:
--------------------------------------------------------------------------------
1 |
2 |
38 |
39 |
54 |
--------------------------------------------------------------------------------
/src/base/mm-loading/mm-loading.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
12 |
13 |
14 |
15 |
16 |
33 |
34 |
87 |
--------------------------------------------------------------------------------
/src/base/mm-no-result/mm-no-result.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
20 |
21 |
35 |
--------------------------------------------------------------------------------
/src/base/mm-progress/mm-progress.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
15 |
16 |
134 |
135 |
179 |
--------------------------------------------------------------------------------
/src/base/mm-toast/index.js:
--------------------------------------------------------------------------------
1 | import TempToast from './mm-toast.vue'
2 |
3 | let instance
4 | let showToast = false
5 | let time // 存储toast显示状态
6 | const mmToast = {
7 | install(Vue, options = {}) {
8 | let opt = TempToast.data() // 获取组件中的默认配置
9 | Object.assign(opt, options) // 合并配置
10 | Vue.prototype.$mmToast = (message, position) => {
11 | if (showToast) {
12 | clearTimeout(time)
13 | instance.vm.visible = showToast = false
14 | document.body.removeChild(instance.vm.$el)
15 | // return;// 如果toast还在,则不再执行
16 | }
17 | if (message) {
18 | opt.message = message // 如果有传message,则使用所传的message
19 | }
20 | if (position) {
21 | opt.position = position // 如果有传type,则使用所传的type
22 | }
23 | let TempToastConstructor = Vue.extend(TempToast)
24 | instance = new TempToastConstructor({
25 | data: opt
26 | })
27 | instance.vm = instance.$mount()
28 | document.body.appendChild(instance.vm.$el)
29 | instance.vm.visible = showToast = true
30 |
31 | time = setTimeout(function() {
32 | instance.vm.visible = showToast = false
33 | document.body.removeChild(instance.vm.$el)
34 | }, opt.duration)
35 | }
36 | }
37 | }
38 |
39 | export default mmToast
40 |
--------------------------------------------------------------------------------
/src/base/mm-toast/mm-toast.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ message }}
5 |
6 |
7 |
8 |
26 |
27 |
74 |
--------------------------------------------------------------------------------
/src/components/lyric/lyric.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | -
6 |
7 |
8 |
9 | - 歌曲名:{{ currentMusic.name }}
10 | - 歌手名:{{ currentMusic.singer }}
11 | - 专辑名:{{ currentMusic.album }}
12 |
13 |
14 | - mmPlayer在线音乐播放器
15 | -
16 |
17 | 茂茂
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
还没有播放音乐哦!
26 |
暂无歌词!
27 |
28 |
33 | {{ item.text }}
34 |
35 |
36 |
歌词加载失败!
37 |
38 |
39 |
40 |
41 |
42 |
102 |
103 |
176 |
--------------------------------------------------------------------------------
/src/components/music-btn/music-btn.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 正在播放
8 | 推荐
12 | 搜索
16 |
20 | 我的歌单
21 |
22 | 歌词
26 |
27 |
28 |
29 |
30 |
33 |
34 |
85 |
--------------------------------------------------------------------------------
/src/components/volume/volume.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
17 |
18 |
19 |
64 |
65 |
83 |
--------------------------------------------------------------------------------
/src/config.js:
--------------------------------------------------------------------------------
1 | // 版本号
2 | export const VERSION = process.env.VUE_APP_VERSION
3 |
4 | /**
5 | * 默认歌单ID (正在播放列表)
6 | * 默认云音乐热歌榜 https://music.163.com/#/discover/toplist?id=3778678
7 | * 如需要修改自定义歌单的请修改
8 | * @type {number}
9 | */
10 | export const defaultSheetId = 3778678
11 |
12 | // 默认分页数量
13 | export const defaultLimit = 30
14 |
15 | // 默认背景图(可引入网络图或本地静态图)
16 | const requireAll = requireContext => requireContext.keys().map(requireContext)
17 | const req = require.context('./assets/background', false)
18 | const BG_ARR = requireAll(req)
19 | export const defaultBG = BG_ARR[Math.floor(Math.random() * BG_ARR.length)]
20 |
21 | // 默认音量
22 | export const defaultVolume = 0.8
23 |
24 | /**
25 | * 播放模式
26 | * listLoop: 列表循环
27 | * order:顺序
28 | * loop: 单曲循环
29 | * random: 随机
30 | */
31 | export const playMode = {
32 | listLoop: 0,
33 | order: 1,
34 | random: 2,
35 | loop: 3
36 | }
37 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | // The Vue build version to load with the `import` command
2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias.
3 | // import 'babel-polyfill'
4 | // import '@/utils/hack'
5 | import Vue from 'vue'
6 | import store from './store'
7 | import router from './router'
8 | import App from './App'
9 | import fastclick from 'fastclick'
10 | import mmToast from 'base/mm-toast'
11 | import Icon from 'base/mm-icon/mm-icon'
12 | import VueLazyload from 'vue-lazyload'
13 | import { VERSION } from './config'
14 | import Loading from '@/base/mm-loading/mm-loading.vue'
15 |
16 | import '@/styles/index.less'
17 |
18 | // 优化移动端300ms点击延迟
19 | fastclick.attach(document.body)
20 |
21 | // 弹出层
22 | Vue.use(mmToast)
23 |
24 | // icon 组件
25 | Vue.component(Icon.name, Icon)
26 |
27 | // 懒加载
28 | Vue.use(VueLazyload, {
29 | preLoad: 1,
30 | loading: require('assets/img/default.png')
31 | })
32 |
33 | // 访问版本统计
34 | window._hmt && window._hmt.push(['_setCustomVar', 1, 'version', VERSION, 1])
35 |
36 | const redirectList = ['/music/details', '/music/comment']
37 | router.beforeEach((to, from, next) => {
38 | window._hmt &&
39 | to.path &&
40 | window._hmt.push(['_trackPageview', '/#' + to.fullPath])
41 | if (redirectList.includes(to.path)) {
42 | next()
43 | } else {
44 | document.title =
45 | (to.meta.title && `${to.meta.title} - mmPlayer在线音乐播放器`) ||
46 | 'mmPlayer在线音乐播放器'
47 | next()
48 | }
49 | })
50 |
51 | // 版权信息
52 | // window.mmPlayer = window.mmplayer = `欢迎使用 mmPlayer!
53 | // 当前版本为:V${VERSION}
54 | // 作者:茂茂
55 | // Github:https://github.com/maomao1996/Vue-mmPlayer
56 | // 歌曲来源于网易云音乐 (https://music.163.com)`
57 | // // eslint-disable-next-line no-console
58 | // console.info(`%c${window.mmplayer}`, `color:blue`)
59 | // 动态注册组件
60 | Vue.loading = () => {
61 | let v = new Vue({
62 | store,
63 | router,
64 | render: h => h(Loading)
65 | }).$mount(document.createElement('div'))
66 | document.body.appendChild(v.$el)
67 | return {
68 | close() {
69 | document.body.removeChild(v.$el)
70 | }
71 | }
72 | }
73 |
74 | // eslint-disable-next-line no-new
75 | new Vue({
76 | el: '#mmPlayer',
77 | store,
78 | router,
79 | render: h => h(App)
80 | })
81 |
--------------------------------------------------------------------------------
/src/pages/details/details.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
48 |
49 |
60 |
--------------------------------------------------------------------------------
/src/pages/historyList/historyList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 | 清空列表
12 |
13 |
14 |
20 |
21 |
22 |
23 |
64 |
65 |
89 |
--------------------------------------------------------------------------------
/src/pages/mmPlayer.js:
--------------------------------------------------------------------------------
1 | import { playMode } from '@/config'
2 |
3 | // 重试次数
4 | let retry = 1
5 |
6 | const mmPlayerMusic = {
7 | initAudio(that) {
8 | const ele = that.audioEle
9 | // 音频缓冲事件
10 | ele.onprogress = () => {
11 | try {
12 | if (ele.buffered.length > 0) {
13 | const duration = that.currentMusic.duration
14 | let buffered = 0
15 | ele.buffered.end(0)
16 | buffered =
17 | ele.buffered.end(0) > duration ? duration : ele.buffered.end(0)
18 | that.currentProgress = buffered / duration
19 | }
20 | } catch (e) {}
21 | }
22 | // 开始播放音乐
23 | ele.onplay = () => {
24 | let timer
25 | clearTimeout(timer)
26 | timer = setTimeout(() => {
27 | that.musicReady = true
28 | }, 100)
29 | }
30 | // 获取当前播放时间
31 | ele.ontimeupdate = () => {
32 | that.currentTime = ele.currentTime
33 | }
34 | // 当前音乐播放完毕
35 | ele.onended = () => {
36 | if (that.mode === playMode.loop) {
37 | that.loop()
38 | } else {
39 | that.next()
40 | }
41 | }
42 | // 音乐播放出错
43 | ele.onerror = () => {
44 | if (retry === 0) {
45 | let toastText = '当前音乐不可播放,已自动播放下一曲'
46 | if (that.playlist.length === 1) {
47 | toastText = '没有可播放的音乐哦~'
48 | }
49 | that.$mmToast(toastText)
50 | that.next(true)
51 | } else {
52 | // eslint-disable-next-line no-console
53 | console.log('重试一次')
54 | retry -= 1
55 | ele.url = that.currentMusic.url
56 | ele.load()
57 | }
58 | // console.log('播放出错啦!')
59 | }
60 | // 音乐进度拖动大于加载时重载音乐
61 | ele.onstalled = () => {
62 | ele.load()
63 | that.setPlaying(false)
64 | let timer
65 | clearTimeout(timer)
66 | timer = setTimeout(() => {
67 | that.setPlaying(true)
68 | }, 10)
69 | }
70 | // 将能播放的音乐加入播放历史
71 | ele.oncanplay = () => {
72 | retry = 1
73 | if (
74 | that.historyList.length === 0 ||
75 | that.currentMusic.id !== that.historyList[0].id
76 | ) {
77 | that.setHistory(that.currentMusic)
78 | }
79 | }
80 | // 音频数据不可用时
81 | ele.onstalled = () => {
82 | ele.load()
83 | that.setPlaying(false)
84 | let timer
85 | clearTimeout(timer)
86 | timer = setTimeout(() => {
87 | that.setPlaying(true)
88 | }, 10)
89 | }
90 | // 当音频已暂停时
91 | ele.onpause = () => {
92 | that.setPlaying(false)
93 | }
94 | }
95 | }
96 |
97 | export default mmPlayerMusic
98 |
--------------------------------------------------------------------------------
/src/pages/playList/playList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 | 清空列表
12 |
13 |
14 |
20 |
21 |
22 |
23 |
71 |
72 |
97 |
--------------------------------------------------------------------------------
/src/pages/search/search.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
11 | {{ item.first }}
12 |
13 |
20 |
21 |
28 |
29 |
30 |
31 |
131 |
132 |
178 |
--------------------------------------------------------------------------------
/src/pages/topList/topList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 云音乐特色榜
7 |
8 |
14 |
19 |
20 |
![]()
24 |
25 | {{ item.name }}
26 |
27 |
28 |
29 | 热门歌单
30 |
31 |
37 |
42 |
43 |
![]()
44 |
45 | {{ item.name }}
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
83 |
84 |
150 |
--------------------------------------------------------------------------------
/src/pages/userList/userList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
12 |
17 |
18 | {{ item.name }}
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
92 |
93 |
140 |
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Router from 'vue-router'
3 | Vue.use(Router)
4 |
5 | const routes = [
6 | {
7 | path: '/',
8 | redirect: '/music'
9 | },
10 | {
11 | path: '/music',
12 | component: () => import('pages/music'),
13 | redirect: '/music/playlist',
14 | children: [
15 | {
16 | path: '/music/playlist', // 正在播放列表
17 | component: () => import('pages/playList/playList'),
18 | meta: {
19 | keepAlive: true
20 | }
21 | },
22 | {
23 | path: '/music/userlist', // 我的歌单
24 | component: () => import('pages/userList/userList'),
25 | meta: {
26 | title: '我的歌单',
27 | keepAlive: true
28 | }
29 | },
30 | {
31 | path: '/music/toplist', // 排行榜列表
32 | component: () => import('pages/topList/topList'),
33 | meta: {
34 | title: '排行榜',
35 | keepAlive: true
36 | }
37 | },
38 | {
39 | path: '/music/details/:id', // 音乐详情列表
40 | component: () => import('pages/details/details')
41 | },
42 | {
43 | path: '/music/historylist', // 我听过的列表
44 | component: () => import('pages/historyList/historyList'),
45 | meta: {
46 | title: '我听过的'
47 | }
48 | },
49 | {
50 | path: '/music/search', // 搜索
51 | component: () => import('pages/search/search'),
52 | meta: {
53 | title: '搜索',
54 | keepAlive: true
55 | }
56 | },
57 | {
58 | path: '/music/comment/:id', // 音乐评论
59 | component: () => import('pages/comment/comment'),
60 | meta: {
61 | title: '评论详情'
62 | }
63 | }
64 | ]
65 | }
66 | ]
67 |
68 | export default new Router({
69 | linkActiveClass: 'active',
70 | linkExactActiveClass: 'active',
71 | routes
72 | })
73 |
--------------------------------------------------------------------------------
/src/store/actions.js:
--------------------------------------------------------------------------------
1 | import {
2 | clearHistoryList,
3 | setHistoryList,
4 | removeHistoryList,
5 | setMode,
6 | setUserId
7 | } from '@/utils/storage'
8 | import * as types from './mutation-types'
9 |
10 | function findIndex(list, music) {
11 | return list.findIndex(item => {
12 | return item.id === music.id
13 | })
14 | }
15 |
16 | // 设置播放列表
17 | export const setPlaylist = function({ commit }, { list }) {
18 | commit(types.SET_PLAYLIST, list)
19 | commit(types.SET_ORDERLIST, list)
20 | }
21 |
22 | // 选择播放(会更新整个播放列表)
23 | export const selectPlay = function({ commit }, { list, index }) {
24 | commit(types.SET_PLAYLIST, list)
25 | commit(types.SET_ORDERLIST, list)
26 | commit(types.SET_CURRENTINDEX, index)
27 | commit(types.SET_PLAYING, true)
28 | }
29 | // 选择播放(会插入一条到播放列表)
30 | export const selectAddPlay = function({ commit, state }, music) {
31 | let list = [...state.playlist]
32 | // 查询当前播放列表是否有代插入的音乐,并返回其索引值
33 | let index = findIndex(list, music)
34 | // 当前播放列表有待插入的音乐时,直接改变当前播放音乐的索引值
35 | if (index > -1) {
36 | commit(types.SET_CURRENTINDEX, index)
37 | } else {
38 | list.unshift(music)
39 | commit(types.SET_PLAYLIST, list)
40 | commit(types.SET_ORDERLIST, list)
41 | commit(types.SET_CURRENTINDEX, 0)
42 | }
43 | commit(types.SET_PLAYING, true)
44 | }
45 |
46 | // 清空播放列表
47 | export const clearPlayList = function({ commit }) {
48 | commit(types.SET_PLAYING, false)
49 | commit(types.SET_CURRENTINDEX, -1)
50 | commit(types.SET_PLAYLIST, [])
51 | commit(types.SET_ORDERLIST, [])
52 | }
53 |
54 | // 删除正在播放列表中的歌曲
55 | export const removerPlayListItem = function(
56 | { commit, state },
57 | { list, index }
58 | ) {
59 | let currentIndex = state.currentIndex
60 | if (index < state.currentIndex || list.length === state.currentIndex) {
61 | currentIndex--
62 | commit(types.SET_CURRENTINDEX, currentIndex)
63 | }
64 | commit(types.SET_PLAYLIST, list)
65 | commit(types.SET_ORDERLIST, list)
66 | if (!list.length) {
67 | commit(types.SET_PLAYING, false)
68 | } else {
69 | commit(types.SET_PLAYING, true)
70 | }
71 | }
72 | // 设置播放历史
73 | export const setHistory = function({ commit }, music) {
74 | commit(types.SET_HISTORYLIST, setHistoryList(music))
75 | }
76 | // 删除播放历史
77 | export const removeHistory = function({ commit }, music) {
78 | commit(types.SET_HISTORYLIST, removeHistoryList(music))
79 | }
80 | // 清空播放历史
81 | export const clearHistory = function({ commit }) {
82 | commit(types.SET_HISTORYLIST, clearHistoryList())
83 | }
84 | // 设置播放模式
85 | export const setPlayMode = function({ commit }, mode) {
86 | commit(types.SET_PLAYMODE, setMode(mode))
87 | }
88 | // 设置网易云用户UID
89 | export const setUid = function({ commit }, uid) {
90 | commit(types.SET_UID, setUserId(uid))
91 | }
92 |
--------------------------------------------------------------------------------
/src/store/getters.js:
--------------------------------------------------------------------------------
1 | // audio元素
2 | export const audioEle = state => state.audioEle
3 | // 播放模式
4 | export const mode = state => state.mode
5 | // 播放状态
6 | export const playing = state => state.playing
7 | // 播放列表
8 | export const playlist = state => state.playlist
9 | // 顺序列表
10 | export const orderList = state => state.orderList
11 | // 当前音乐索引
12 | export const currentIndex = state => state.currentIndex
13 | // 当前音乐
14 | export const currentMusic = state => {
15 | return state.playlist[state.currentIndex] || {}
16 | }
17 | // 播放历史列表
18 | export const historyList = state => state.historyList
19 | // 网易云用户UID
20 | export const uid = state => state.uid
21 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 | import state from './state'
4 | import * as getters from './getters'
5 | import * as actions from './actions'
6 | import mutations from './mutations'
7 | // vuex调试
8 | import createLogger from 'vuex/dist/logger'
9 | const debug = process.env.NODE_ENV !== 'production'
10 |
11 | Vue.use(Vuex)
12 |
13 | export default new Vuex.Store({
14 | state,
15 | getters,
16 | mutations,
17 | actions,
18 | // vuex调试
19 | strict: debug,
20 | plugins: debug ? [createLogger()] : []
21 | })
22 |
--------------------------------------------------------------------------------
/src/store/mutation-types.js:
--------------------------------------------------------------------------------
1 | export const SET_AUDIOELE = 'SET_AUDIOELE' // 修改audio元素
2 | export const SET_PLAYMODE = 'SET_PLAYMODE' // 修改播放模式
3 | export const SET_PLAYING = 'SET_PLAYING' // 修改播放状态
4 | export const SET_PLAYLIST = 'SET_PLAYLIST' // 修改播放列表
5 | export const SET_ORDERLIST = 'SET_ORDERLIST' // 修改顺序列表
6 | export const SET_CURRENTINDEX = 'SET_CURRENTINDEX' // 修改当前音乐索引
7 | export const SET_HISTORYLIST = 'SET_HISTORYLIST' // 修改播放历史列表
8 | export const SET_UID = 'SET_UID' // 修改网易云用户UID
9 |
--------------------------------------------------------------------------------
/src/store/mutations.js:
--------------------------------------------------------------------------------
1 | import * as types from './mutation-types'
2 |
3 | const mutations = {
4 | // 修改audio元素
5 | [types.SET_AUDIOELE](state, audioEle) {
6 | state.audioEle = audioEle
7 | },
8 | // 修改播放模式
9 | [types.SET_PLAYMODE](state, mode) {
10 | state.mode = mode
11 | },
12 | // 修改播放状态
13 | [types.SET_PLAYING](state, playing) {
14 | state.playing = playing
15 | },
16 | // 修改播放列表
17 | [types.SET_PLAYLIST](state, playlist) {
18 | state.playlist = playlist
19 | },
20 | // 修改顺序列表
21 | [types.SET_ORDERLIST](state, orderList) {
22 | state.orderList = orderList
23 | },
24 | // 修改当前音乐索引
25 | [types.SET_CURRENTINDEX](state, currentIndex) {
26 | state.currentIndex = currentIndex
27 | },
28 | // 修改播放历史列表
29 | [types.SET_HISTORYLIST](state, historyList) {
30 | state.historyList = historyList
31 | },
32 | // 修改网易云用户UID
33 | [types.SET_UID](state, uid) {
34 | state.uid = uid
35 | }
36 | }
37 |
38 | export default mutations
39 |
--------------------------------------------------------------------------------
/src/store/state.js:
--------------------------------------------------------------------------------
1 | import { playMode } from '@/config'
2 | import { getHistoryList, getMode, getUserId } from '@/utils/storage'
3 |
4 | const state = {
5 | audioEle: null, // audio元素
6 | mode: Number(getMode()) || playMode.listLoop, // 播放模式,默认列表循环
7 | playing: false, // 播放状态
8 | playlist: [], // 播放列表
9 | orderList: [], // 顺序列表
10 | currentIndex: -1, // 当前音乐索引
11 | historyList: getHistoryList() || [], // 播放历史列表
12 | uid: getUserId() || null // 网易云用户UID
13 | }
14 |
15 | export default state
16 |
--------------------------------------------------------------------------------
/src/styles/index.less:
--------------------------------------------------------------------------------
1 | @import 'reset';
2 | @import 'var';
3 | html,
4 | body,
5 | #app {
6 | width: 100%;
7 | height: 100%;
8 | overflow: hidden;
9 | }
10 |
11 | body {
12 | min-width: 320px;
13 | font-family: Arial;
14 | }
15 |
16 | #app {
17 | position: relative;
18 | }
19 |
20 | .cover-img {
21 | width: 100%;
22 | height: 100%;
23 | object-fit: cover;
24 | }
25 |
26 | //浮动
27 | .fl {
28 | float: left;
29 | }
30 |
31 | .fr {
32 | float: right;
33 | }
34 |
35 | .pointer {
36 | cursor: pointer;
37 | }
38 |
39 | .hover {
40 | color: @text_color;
41 | cursor: pointer;
42 | &:hover {
43 | color: @text_color_active;
44 | }
45 | }
46 |
47 | .text-left {
48 | text-align: left;
49 | }
50 |
51 | .clearfix {
52 | &:after {
53 | display: block;
54 | content: '';
55 | clear: both;
56 | }
57 | }
58 |
59 | //滚动条
60 | ::-webkit-scrollbar {
61 | /*滚动条整体部分,其中的属性有width,height,background,border(就和一个块级元素一样)等*/
62 | background-color: rgba(0, 0, 0, 0.3);
63 | width: 5px; //纵向滚动条
64 | border-radius: 10px;
65 | }
66 |
67 | ::-webkit-scrollbar-button {
68 | /*滚动条两端的按钮。可以用display:none让其不显示,也可以添加背景图片,颜色改变显示效果。*/
69 | display: none;
70 | }
71 |
72 | ::-webkit-scrollbar-track {
73 | /*外层轨道。可以用display:none让其不显示,也可以添加背景图片,颜色改变显示效果。*/
74 | display: none;
75 | //background-color: rgba(255, 255, 255, 0.1);
76 | border-radius: 10px;
77 | }
78 |
79 | ::-webkit-scrollbar-track-piece {
80 | /*内层轨道,滚动条中间部分(除去)。*/
81 | //background-color: rgba(255, 255, 255, .1);
82 | border-radius: 10px;
83 | }
84 |
85 | ::-webkit-scrollbar-thumb {
86 | /*滚动条里面可以拖动的那部分*/
87 | background-color: rgba(255, 255, 255, 0.5);
88 | border-radius: 10px;
89 | }
90 |
91 | ::-webkit-scrollbar-corner {
92 | border-radius: 10px;
93 | }
94 |
95 | ::-webkit-resizer {
96 | /*定义右下角拖动块的样式*/
97 | border-radius: 10px;
98 | }
99 |
--------------------------------------------------------------------------------
/src/styles/mixin.less:
--------------------------------------------------------------------------------
1 | // 显示省略号
2 | .no-wrap() {
3 | text-overflow: ellipsis;
4 | overflow: hidden;
5 | white-space: nowrap;
6 | }
7 |
8 | .flex-center(@direction: row) {
9 | display: flex;
10 | flex-direction: @direction;
11 | justify-content: center;
12 | align-items: center;
13 | }
14 |
--------------------------------------------------------------------------------
/src/styles/reset.less:
--------------------------------------------------------------------------------
1 | html,
2 | body,
3 | div,
4 | span,
5 | applet,
6 | object,
7 | iframe,
8 | h1,
9 | h2,
10 | h3,
11 | h4,
12 | h5,
13 | h6,
14 | p,
15 | blockquote,
16 | pre,
17 | a,
18 | abbr,
19 | acronym,
20 | address,
21 | big,
22 | cite,
23 | code,
24 | del,
25 | dfn,
26 | em,
27 | img,
28 | ins,
29 | kbd,
30 | q,
31 | s,
32 | samp,
33 | small,
34 | strike,
35 | strong,
36 | sub,
37 | sup,
38 | tt,
39 | var,
40 | b,
41 | u,
42 | i,
43 | center,
44 | dl,
45 | dt,
46 | dd,
47 | ol,
48 | ul,
49 | li,
50 | fieldset,
51 | form,
52 | label,
53 | legend,
54 | table,
55 | caption,
56 | tbody,
57 | tfoot,
58 | thead,
59 | tr,
60 | th,
61 | td,
62 | article,
63 | aside,
64 | canvas,
65 | details,
66 | embed,
67 | figure,
68 | figcaption,
69 | footer,
70 | header,
71 | hgroup,
72 | menu,
73 | nav,
74 | output,
75 | ruby,
76 | section,
77 | summary,
78 | time,
79 | mark,
80 | audio,
81 | video {
82 | margin: 0;
83 | padding: 0;
84 | border: 0;
85 | font-size: 100%;
86 | font: inherit;
87 | vertical-align: baseline;
88 | -webkit-tap-highlight-color: transparent;
89 | }
90 |
91 | /* HTML5 display-role reset for older browsers */
92 | article,
93 | aside,
94 | details,
95 | figcaption,
96 | figure,
97 | footer,
98 | header,
99 | hgroup,
100 | menu,
101 | nav,
102 | section {
103 | display: block;
104 | }
105 |
106 | body {
107 | line-height: 1;
108 | }
109 |
110 | ol,
111 | ul {
112 | list-style: none;
113 | }
114 |
115 | blockquote,
116 | q {
117 | quotes: none;
118 | }
119 |
120 | blockquote:before,
121 | blockquote:after,
122 | q:before,
123 | q:after {
124 | content: '';
125 | content: none;
126 | }
127 |
128 | table {
129 | border-collapse: collapse;
130 | border-spacing: 0;
131 | }
132 |
133 | a {
134 | text-decoration: none;
135 | color: inherit;
136 | }
137 |
138 | input[type='number']::-webkit-inner-spin-button,
139 | input[type='number']::-webkit-outer-spin-button {
140 | -webkit-appearance: none;
141 | }
142 |
--------------------------------------------------------------------------------
/src/styles/var.less:
--------------------------------------------------------------------------------
1 | //字体颜色定义规范
2 | @text_color: rgba(255, 255, 255, 0.6);
3 | @text_color_active: #fff; //重点部分
4 |
5 | //active颜色
6 | @active_color: #fff;
7 |
8 | //遮罩层颜色
9 | @mask_color: rgba(0, 0, 0, 0.4);
10 |
11 | //loading背景颜色
12 | @load_bg_color: rgba(0, 0, 0, 0.2);
13 |
14 | //header背景颜色
15 | @header_bg_color: rgba(0, 0, 0, 0.3);
16 |
17 | //search-head
18 | @search_bg_coloe: rgba(0, 0, 0, 0.2);
19 |
20 | //dialog相关
21 | @dialog_bg_color: rgba(0, 0, 0, 0.5);
22 | @dialog_content_bg_color: rgba(0, 0, 0, 0.6);
23 | @dialog_text_color: rgba(255, 255, 255, 0.7);
24 | @dialog_line_color: rgba(0, 0, 0, 0.35);
25 |
26 | //btn相关
27 | @btn_color: rgba(255, 255, 255, 0.6);
28 | @btn_color_active: #fff;
29 |
30 | //歌词高亮颜色
31 | @lyric_color_active: #40ce8f;
32 |
33 | //进度条
34 | @bar_color: rgba(255, 255, 255, 0.15);
35 | @line_color: #fff;
36 | @dot_color: #fff;
37 |
38 | //列表
39 | @list_head_line_color: rgba(255, 255, 255, 0.8);
40 | @list_item_line_color: rgba(255, 255, 255, 0.1);
41 |
42 | //评论
43 | @comment_head_line_color: rgba(255, 255, 255, 0.8);
44 | @comment_item_line_color: rgba(255, 255, 255, 0.1);
45 | @comment_replied_line_color: rgba(255, 255, 255, 0.3);
46 |
47 | //字体大小定义规范
48 | @font_size_small: 12px;
49 | @font_size_medium: 14px;
50 | @font_size_medium_x: 16px;
51 | @font_size_large: 18px;
52 | @font_size_large_x: 22px;
53 |
--------------------------------------------------------------------------------
/src/utils/axios.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import Vue from 'vue'
3 |
4 | const request = axios.create({
5 | baseURL: process.env.VUE_APP_BASE_API_URL
6 | })
7 |
8 | request.interceptors.response.use(
9 | response => {
10 | window.response = response
11 |
12 | if (response.status === 200 && response.data.code === 200) {
13 | return response.data
14 | }
15 | return Promise.reject(response)
16 | },
17 | e => {
18 | Vue.prototype.$mmToast(e.message)
19 | throw e
20 | }
21 | )
22 |
23 | export default request
24 |
--------------------------------------------------------------------------------
/src/utils/hack.js:
--------------------------------------------------------------------------------
1 | // hack for global nextTick
2 |
3 | function noop() {}
4 |
5 | window.MessageChannel = noop
6 | window.setImmediate = noop
7 |
--------------------------------------------------------------------------------
/src/utils/mixin.js:
--------------------------------------------------------------------------------
1 | import { mapGetters, mapMutations, mapActions } from 'vuex'
2 |
3 | /**
4 | * 歌曲列表
5 | */
6 | export const listMixin = {
7 | computed: {
8 | ...mapGetters(['playing', 'currentMusic'])
9 | },
10 | methods: {
11 | selectItem(item, index) {
12 | if (item.id === this.currentMusic.id && this.playing) {
13 | this.setPlaying(false)
14 | } else {
15 | this.selectPlay({
16 | list: this.list,
17 | index
18 | })
19 | }
20 | },
21 | ...mapMutations({
22 | setPlaying: 'SET_PLAYING'
23 | }),
24 | ...mapActions(['selectPlay'])
25 | }
26 | }
27 |
28 | /**
29 | * loading状态
30 | * @type {{data(): *, methods: {_hideLoad(): void}}}
31 | */
32 | export const loadMixin = {
33 | data() {
34 | return {
35 | mmLoadShow: true // loading状态
36 | }
37 | },
38 | methods: {
39 | _hideLoad() {
40 | let timer
41 | clearTimeout(timer)
42 | timer = setTimeout(() => {
43 | this.mmLoadShow = false
44 | }, 200)
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/utils/song.js:
--------------------------------------------------------------------------------
1 | import { toHttps } from './util'
2 |
3 | function filterSinger(singers) {
4 | let arr = []
5 | singers.forEach(item => {
6 | arr.push(item.name)
7 | })
8 | return arr.join('/')
9 | }
10 |
11 | export class Song {
12 | constructor({ id, name, singer, album, image, duration, url }) {
13 | this.id = id
14 | this.name = name
15 | this.singer = singer
16 | this.album = album
17 | this.image = image
18 | this.duration = duration
19 | this.url = url
20 | }
21 | }
22 |
23 | export function createPlayList(music) {
24 | return new Song({
25 | id: music.id,
26 | name: music.name,
27 | singer: music.artists.length > 0 && filterSinger(music.artists),
28 | album: music.album.name,
29 | image: toHttps(music.album.picUrl) || null,
30 | duration: music.duration / 1000,
31 | url: `https://music.163.com/song/media/outer/url?id=${music.id}.mp3`
32 | })
33 | }
34 |
35 | export function createTopList(music) {
36 | return new Song({
37 | id: music.id,
38 | name: music.name,
39 | singer: music.ar.length > 0 && filterSinger(music.ar),
40 | album: music.al.name,
41 | image: toHttps(music.al.picUrl),
42 | duration: music.dt / 1000,
43 | url: `https://music.163.com/song/media/outer/url?id=${music.id}.mp3`
44 | })
45 | }
46 |
47 | // 歌曲数据格式化
48 | const formatSongs = function formatPlayList(list) {
49 | let Songs = []
50 | list.forEach(item => {
51 | const musicData = item
52 | if (musicData.id) {
53 | Songs.push(createPlayList(musicData))
54 | }
55 | })
56 | return Songs
57 | }
58 |
59 | export const formatTopSongs = function formatTopList(list) {
60 | let Songs = []
61 | list.forEach(item => {
62 | const musicData = item
63 | if (musicData.id) {
64 | Songs.push(createTopList(musicData))
65 | }
66 | })
67 | return Songs
68 | }
69 |
70 | export default formatSongs
71 |
--------------------------------------------------------------------------------
/src/utils/storage.js:
--------------------------------------------------------------------------------
1 | import { defaultVolume } from '@/config'
2 |
3 | const _storage = window.localStorage
4 | const storage = {
5 | get(key, data = []) {
6 | if (_storage) {
7 | return _storage.getItem(key)
8 | ? Array.isArray(data)
9 | ? JSON.parse(_storage.getItem(key))
10 | : _storage.getItem(key)
11 | : data
12 | }
13 | },
14 | set(key, val) {
15 | if (_storage) {
16 | _storage.setItem(key, val)
17 | }
18 | },
19 | clear(key) {
20 | if (_storage) {
21 | _storage.removeItem(key)
22 | }
23 | }
24 | }
25 |
26 | /**
27 | * 播放历史
28 | * @type HISTORYLIST_KEY:key值
29 | * HistoryListMAX:最大长度
30 | */
31 | const HISTORYLIST_KEY = '__mmPlayer_historyList__'
32 | const HistoryListMAX = 200
33 | // 获取播放历史
34 | export function getHistoryList() {
35 | return storage.get(HISTORYLIST_KEY)
36 | }
37 |
38 | // 更新播放历史
39 | export function setHistoryList(music) {
40 | let list = storage.get(HISTORYLIST_KEY)
41 | const index = list.findIndex(item => {
42 | return item.id === music.id
43 | })
44 | if (index === 0) {
45 | return list
46 | }
47 | if (index > 0) {
48 | list.splice(index, 1)
49 | }
50 | list.unshift(music)
51 | if (HistoryListMAX && list.length > HistoryListMAX) {
52 | list.pop()
53 | }
54 | storage.set(HISTORYLIST_KEY, JSON.stringify(list))
55 | return list
56 | }
57 |
58 | // 删除一条播放历史
59 | export function removeHistoryList(music) {
60 | storage.set(HISTORYLIST_KEY, JSON.stringify(music))
61 | return music
62 | }
63 |
64 | // 清空播放历史
65 | export function clearHistoryList() {
66 | storage.clear(HISTORYLIST_KEY)
67 | return []
68 | }
69 |
70 | /**
71 | * 播放模式
72 | * @type MODE_KEY:key值
73 | * HistoryListMAX:最大长度
74 | */
75 | const MODE_KEY = '__mmPlayer_mode__'
76 | // 获取播放模式
77 | export function getMode() {
78 | return storage.get(MODE_KEY, null)
79 | }
80 | // 修改播放模式
81 | export function setMode(mode) {
82 | storage.set(MODE_KEY, mode)
83 | return mode
84 | }
85 |
86 | // 获取用户uid
87 | export function getUserId() {
88 | const query = new URLSearchParams(location.search)
89 | const uid = query.get('uid')
90 | if (/^\d+$/.test(uid)) return uid
91 | }
92 | // 修改用户uid
93 | export function setUserId(uid) {
94 | return uid
95 | }
96 |
97 | /**
98 | * 版本号
99 | * @type VERSION_KEY:key值
100 | */
101 | const VERSION_KEY = '__mmPlayer_version__'
102 | // 获取版本号
103 | export function getVersion() {
104 | let version = storage.get(VERSION_KEY, null)
105 | return Array.isArray(version) ? null : version
106 | }
107 | // 修改版本号
108 | export function setVersion(version) {
109 | storage.set(VERSION_KEY, version)
110 | return version
111 | }
112 |
113 | /**
114 | * 音量
115 | * @type VOLUME_KEY:key值
116 | */
117 | const VOLUME_KEY = '__mmPlayer_volume__'
118 | // 获取音量
119 | export function getVolume() {
120 | const volume = storage.get(VOLUME_KEY, defaultVolume)
121 | return Number(volume)
122 | }
123 | // 修改音量
124 | export function setVolume(volume) {
125 | storage.set(VOLUME_KEY, volume)
126 | return volume
127 | }
128 |
--------------------------------------------------------------------------------
/src/utils/util.js:
--------------------------------------------------------------------------------
1 | // 随机排序数组/洗牌函数 https://github.com/lodash/lodash/blob/master/shuffle.js
2 | function copyArray(source, array) {
3 | let index = -1
4 | const length = source.length
5 | array || (array = new Array(length))
6 | while (++index < length) {
7 | array[index] = source[index]
8 | }
9 | return array
10 | }
11 |
12 | export const randomSortArray = function shuffle(array) {
13 | const length = array == null ? 0 : array.length
14 | if (!length) {
15 | return []
16 | }
17 | let index = -1
18 | const lastIndex = length - 1
19 | const result = copyArray(array)
20 | while (++index < length) {
21 | const rand = index + Math.floor(Math.random() * (lastIndex - index + 1))
22 | const value = result[rand]
23 | result[rand] = result[index]
24 | result[index] = value
25 | }
26 | return result
27 | }
28 |
29 | // 防抖函数
30 | export function debounce(func, delay) {
31 | let timer
32 | return function(...args) {
33 | if (timer) {
34 | clearTimeout(timer)
35 | }
36 | timer = setTimeout(() => {
37 | func.apply(this, args)
38 | }, delay)
39 | }
40 | }
41 |
42 | // 补0函数
43 | export function addZero(s) {
44 | return s < 10 ? '0' + s : s
45 | }
46 |
47 | // 歌词解析
48 | const timeExp = /\[(\d{2,}):(\d{2})(?:\.(\d{2,3}))?]/g
49 | export function parseLyric(lrc) {
50 | const lines = lrc.split('\n')
51 | const lyric = []
52 | for (let i = 0; i < lines.length; i++) {
53 | const line = lines[i]
54 | const result = timeExp.exec(line)
55 | if (!result) {
56 | continue
57 | }
58 | const text = line.replace(timeExp, '').trim()
59 | if (text) {
60 | lyric.push({
61 | time: (result[1] * 6e4 + result[2] * 1e3 + (result[3] || 0) * 1) / 1e3,
62 | text
63 | })
64 | }
65 | }
66 | return lyric
67 | }
68 |
69 | // 时间格式化
70 | export function format(value) {
71 | let minute = Math.floor(value / 60)
72 | let second = Math.floor(value % 60)
73 | return `${addZero(minute)}:${addZero(second)}`
74 | }
75 |
76 | /**
77 | * https://github.com/videojs/video.js/blob/master/src/js/utils/promise.js
78 | * Silence a Promise-like object.
79 | *
80 | * This is useful for avoiding non-harmful, but potentially confusing "uncaught
81 | * play promise" rejection error messages.
82 | *
83 | * @param {Object} value
84 | * An object that may or may not be `Promise`-like.
85 | */
86 | export function isPromise(v) {
87 | return v !== undefined && v !== null && typeof v.then === 'function'
88 | }
89 |
90 | export function silencePromise(value) {
91 | if (isPromise(value)) {
92 | value.then(null, () => {})
93 | }
94 | }
95 |
96 | // 判断 string 类型
97 | export function isString(v) {
98 | return typeof v === 'string'
99 | }
100 |
101 | // http 链接转化成 https
102 | export function toHttps(url) {
103 | if (!isString(url)) {
104 | return url
105 | }
106 | return url.replace('http://', 'https://')
107 | }
108 |
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const dayjs = require('dayjs')
3 |
4 | function resolve(dir) {
5 | return path.join(__dirname, dir)
6 | }
7 |
8 | const isEnvProduction = process.env.NODE_ENV === 'production'
9 |
10 | // 注入版本信息
11 | process.env.VUE_APP_VERSION = require('./package.json').version
12 | // 注入版本更新时间
13 | process.env.VUE_APP_UPDATE_TIME = dayjs()
14 | .locale('zh-cn')
15 | .format('YYYY-MM-DD')
16 |
17 | module.exports = {
18 | publicPath: '',
19 | outputDir: './custom_components/ha_cloud_music/dist',
20 | chainWebpack(config) {
21 | config.resolve.alias
22 | .set('api', resolve('src/api'))
23 | .set('assets', resolve('src/assets'))
24 | .set('base', resolve('src/base'))
25 | .set('components', resolve('src/components'))
26 | .set('pages', resolve('src/pages'))
27 | config.plugin('html').tap(args => {
28 | if (isEnvProduction) {
29 | args[0].minify.minifyJS = true
30 | args[0].minify.minifyCSS = true
31 | }
32 | return args
33 | })
34 | },
35 | pluginOptions: {
36 | 'style-resources-loader': {
37 | preProcessor: 'less',
38 | patterns: [
39 | resolve('src/styles/var.less'),
40 | resolve('src/styles/mixin.less')
41 | ]
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------