├── .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 | 27 | 28 |
mmPlayer | 温馨提示
很抱歉!为了更好的体验,本站限制以下浏览器访问:
IE浏览器和使用IE内核的浏览器
解决办法:下载其他主流浏览器或者切换浏览器内核为极速内核
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 |
    151 |
    152 |
    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 | 27 | 28 |
    mmPlayer | 温馨提示
    很抱歉!为了更好的体验,本站限制以下浏览器访问:
    IE浏览器和使用IE内核的浏览器
    解决办法:下载其他主流浏览器或者切换浏览器内核为极速内核
    29 |
    30 | 31 | 32 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 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 | 15 | 16 | 33 | 34 | 87 | -------------------------------------------------------------------------------- /src/base/mm-no-result/mm-no-result.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 20 | 21 | 35 | -------------------------------------------------------------------------------- /src/base/mm-progress/mm-progress.vue: -------------------------------------------------------------------------------- 1 | 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 | 7 | 8 | 26 | 27 | 74 | -------------------------------------------------------------------------------- /src/components/lyric/lyric.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 102 | 103 | 176 | -------------------------------------------------------------------------------- /src/components/music-btn/music-btn.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 33 | 34 | 85 | -------------------------------------------------------------------------------- /src/components/volume/volume.vue: -------------------------------------------------------------------------------- 1 | 2 | 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 | 8 | 9 | 48 | 49 | 60 | -------------------------------------------------------------------------------- /src/pages/historyList/historyList.vue: -------------------------------------------------------------------------------- 1 | 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 | 22 | 23 | 71 | 72 | 97 | -------------------------------------------------------------------------------- /src/pages/search/search.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 131 | 132 | 178 | -------------------------------------------------------------------------------- /src/pages/topList/topList.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 83 | 84 | 150 | -------------------------------------------------------------------------------- /src/pages/userList/userList.vue: -------------------------------------------------------------------------------- 1 | 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 | } --------------------------------------------------------------------------------