├── src ├── renderer │ ├── plugins │ │ └── index.js │ ├── assets │ │ ├── images │ │ │ ├── jqbg.jpg │ │ │ ├── xnkl.jpg │ │ │ └── myzcbg.jpeg │ │ └── styles │ │ │ ├── layout.less │ │ │ ├── reset.less │ │ │ └── index.less │ ├── config │ │ └── index.js │ ├── utils │ │ ├── env.js │ │ ├── music │ │ │ ├── options.js │ │ │ ├── kw │ │ │ │ ├── pic.js │ │ │ │ ├── api-temp.js │ │ │ │ ├── lyric.js │ │ │ │ ├── util.js │ │ │ │ ├── api-internal.js │ │ │ │ ├── api-test.js │ │ │ │ ├── tempSearch.js │ │ │ │ ├── index.js │ │ │ │ └── musicSearch.js │ │ │ ├── utils.js │ │ │ ├── mg │ │ │ │ ├── index.js │ │ │ │ ├── api-test.js │ │ │ │ ├── pic.js │ │ │ │ ├── lyric.js │ │ │ │ ├── leaderboard.js │ │ │ │ └── musicSearch.js │ │ │ ├── wy │ │ │ │ ├── index.js │ │ │ │ ├── lyric.js │ │ │ │ ├── musicInfo.js │ │ │ │ ├── api-test.js │ │ │ │ ├── utils │ │ │ │ │ └── crypto.js │ │ │ │ ├── musicSearch.js │ │ │ │ └── leaderboard.js │ │ │ ├── bd │ │ │ │ ├── api-test.js │ │ │ │ ├── musicInfo.js │ │ │ │ ├── index.js │ │ │ │ ├── api-internal.js │ │ │ │ ├── musicSearch.js │ │ │ │ └── leaderboard.js │ │ │ ├── tx │ │ │ │ ├── index.js │ │ │ │ ├── lyric.js │ │ │ │ ├── api-test.js │ │ │ │ ├── api-internal.js │ │ │ │ └── musicSearch.js │ │ │ ├── kg │ │ │ │ ├── index.js │ │ │ │ ├── lyric.js │ │ │ │ ├── pic.js │ │ │ │ ├── api-test.js │ │ │ │ ├── api-internal.js │ │ │ │ ├── musicSearch.js │ │ │ │ └── leaderboard.js │ │ │ ├── index.js │ │ │ └── api-source.js │ │ ├── download │ │ │ ├── util.js │ │ │ └── index.js │ │ └── message.js │ ├── components │ │ ├── index.js │ │ ├── core │ │ │ ├── View.vue │ │ │ └── Aside.vue │ │ └── material │ │ │ ├── Btn.vue │ │ │ ├── InputRange.vue │ │ │ ├── DownloadMultipleModal.vue │ │ │ ├── Input.vue │ │ │ ├── listAddMultipleModal.vue │ │ │ ├── DownloadModal.vue │ │ │ ├── listAddModal.vue │ │ │ ├── FlowBtn.vue │ │ │ ├── Select.vue │ │ │ ├── ListButtons.vue │ │ │ ├── Modal.vue │ │ │ └── Checkbox.vue │ ├── main.js │ ├── store │ │ ├── modules │ │ │ ├── index.js │ │ │ ├── leaderboard.js │ │ │ ├── player.js │ │ │ ├── list.js │ │ │ ├── search.js │ │ │ └── songList.js │ │ ├── getters.js │ │ ├── index.js │ │ ├── actions.js │ │ ├── mutations.js │ │ └── state.js │ └── route │ │ ├── paths.js │ │ └── index.js ├── main │ ├── events │ │ ├── getEnvParams.js │ │ ├── setWindowSize.js │ │ ├── musicMeta.js │ │ ├── restartWindow.js │ │ ├── clearCache.js │ │ ├── getCacheSize.js │ │ ├── progressBar.js │ │ ├── selectDir.js │ │ ├── setIgnoreMouseEvent.js │ │ ├── showSaveDialog.js │ │ ├── appName.js │ │ ├── index.js │ │ ├── trafficLight.js │ │ └── request.js │ ├── utils │ │ ├── musicMeta.js │ │ ├── index.js │ │ ├── flacMeta.js │ │ ├── mp3Meta.js │ │ └── autoUpdate.js │ ├── index.dev.js │ └── index.js ├── common │ ├── utils.js │ ├── error.js │ ├── config.js │ └── ipc.js └── index.pug ├── licenses ├── license_zh.txt └── license_en.txt ├── doc └── images │ ├── app.png │ └── icon.png ├── resources └── icons │ ├── 256x256.ico │ ├── 512x512.icns │ └── 512x512.png ├── postcss.config.js ├── publish ├── utils │ ├── cosConfig.js │ ├── clearAssets.js │ ├── packAssets.js │ ├── compileAssets.js │ ├── copyFile.js │ ├── githubRelease.js │ ├── index.js │ ├── updateChangeLog.js │ └── cos.js ├── changeLog.md └── index.js ├── .editorconfig ├── .appveyor.yml ├── .babelrc ├── .github └── ISSUE_TEMPLATE │ ├── ----.md │ └── --bug.md ├── .eslintrc ├── .travis.yml ├── .gitignore ├── FAQ.md └── README.md /src/renderer/plugins/index.js: -------------------------------------------------------------------------------- 1 | // import './axios' 2 | -------------------------------------------------------------------------------- /licenses/license_zh.txt: -------------------------------------------------------------------------------- 1 | 本程序仅用于学习交流使用! 2 | 请勿用于商业用途!! 3 | 使用本软件造成的一切后果由使用者承担! 4 | 5 | By: 落雪无痕 6 | -------------------------------------------------------------------------------- /doc/images/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghostTM2013/lyswhut-lx-music-desktop/HEAD/doc/images/app.png -------------------------------------------------------------------------------- /doc/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghostTM2013/lyswhut-lx-music-desktop/HEAD/doc/images/icon.png -------------------------------------------------------------------------------- /resources/icons/256x256.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghostTM2013/lyswhut-lx-music-desktop/HEAD/resources/icons/256x256.ico -------------------------------------------------------------------------------- /resources/icons/512x512.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghostTM2013/lyswhut-lx-music-desktop/HEAD/resources/icons/512x512.icns -------------------------------------------------------------------------------- /resources/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghostTM2013/lyswhut-lx-music-desktop/HEAD/resources/icons/512x512.png -------------------------------------------------------------------------------- /src/renderer/assets/images/jqbg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghostTM2013/lyswhut-lx-music-desktop/HEAD/src/renderer/assets/images/jqbg.jpg -------------------------------------------------------------------------------- /src/renderer/assets/images/xnkl.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghostTM2013/lyswhut-lx-music-desktop/HEAD/src/renderer/assets/images/xnkl.jpg -------------------------------------------------------------------------------- /src/renderer/assets/images/myzcbg.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghostTM2013/lyswhut-lx-music-desktop/HEAD/src/renderer/assets/images/myzcbg.jpeg -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const autoprefixer = require('autoprefixer') 2 | 3 | module.exports = { 4 | plugins: [ 5 | autoprefixer(), 6 | ], 7 | } 8 | -------------------------------------------------------------------------------- /publish/utils/cosConfig.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | secretId: '', 3 | secretKey: '', 4 | 5 | bucket: '', // 存储桶 6 | region: '', // 区域 7 | prefix: '', // 路径 8 | } 9 | -------------------------------------------------------------------------------- /src/main/events/getEnvParams.js: -------------------------------------------------------------------------------- 1 | const { mainHandle } = require('../../common/ipc') 2 | 3 | mainHandle('getEnvParams', async(event, options) => { 4 | return global.envParams 5 | }) 6 | 7 | -------------------------------------------------------------------------------- /src/renderer/config/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | development: { 3 | 4 | }, 5 | production: { 6 | 7 | }, 8 | ajax: { 9 | timeout: 15000, // ajax请求超时时间 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /licenses/license_en.txt: -------------------------------------------------------------------------------- 1 | This program is only for learning to communicate! 2 | Do not use for commercial purposes! ! 3 | All consequences of using this software are borne by the user! 4 | 5 | By: lyswhut 6 | -------------------------------------------------------------------------------- /publish/utils/clearAssets.js: -------------------------------------------------------------------------------- 1 | const del = require('del') 2 | // const copyFile = require('./copyFile') 3 | 4 | module.exports = () => { 5 | del.sync(['publish/assets/*']) 6 | // return copyFile(false) 7 | } 8 | 9 | -------------------------------------------------------------------------------- /src/renderer/utils/env.js: -------------------------------------------------------------------------------- 1 | const isDev = process.env.NODE_ENV === 'development' 2 | 3 | export const debug = isDev && true 4 | export const debugRequest = isDev && false 5 | export const debugDownload = isDev && false 6 | -------------------------------------------------------------------------------- /src/renderer/utils/music/options.js: -------------------------------------------------------------------------------- 1 | export const bHh = '624868746c' 2 | 3 | export const headers = { 4 | 'User-Agent': 'lx-music request', 5 | [bHh]: [bHh], 6 | } 7 | 8 | 9 | export const timeout = 15000 10 | -------------------------------------------------------------------------------- /src/main/events/setWindowSize.js: -------------------------------------------------------------------------------- 1 | const { mainOn } = require('../../common/ipc') 2 | 3 | mainOn('setWindowSize', (event, options) => { 4 | if (!global.mainWindow) return 5 | global.mainWindow.setBounds(options) 6 | }) 7 | 8 | -------------------------------------------------------------------------------- /src/main/events/musicMeta.js: -------------------------------------------------------------------------------- 1 | const { mainOn } = require('../../common/ipc') 2 | const { setMeta } = require('../utils/musicMeta') 3 | 4 | mainOn('setMusicMeta', (event, { filePath, meta }) => { 5 | setMeta(filePath, meta) 6 | }) 7 | -------------------------------------------------------------------------------- /src/common/utils.js: -------------------------------------------------------------------------------- 1 | const log = require('electron-log') 2 | 3 | exports.isLinux = process.platform == 'linux' 4 | exports.isWin = process.platform == 'win32' 5 | exports.isMac = process.platform == 'darwin' 6 | 7 | 8 | exports.log = log 9 | -------------------------------------------------------------------------------- /src/renderer/utils/download/util.js: -------------------------------------------------------------------------------- 1 | 2 | exports.STATUS = { 3 | idle: 'IDLE', 4 | init: 'INIT', 5 | running: 'RUNNING', 6 | paused: 'PAUSED', 7 | stopped: 'STOPPED', 8 | completed: 'COMPLETED', 9 | error: 'ERROR', 10 | failed: 'FAILED', 11 | } 12 | -------------------------------------------------------------------------------- /src/main/events/restartWindow.js: -------------------------------------------------------------------------------- 1 | const { mainOn } = require('../../common/ipc') 2 | 3 | 4 | mainOn('restartWindow', (event, name) => { 5 | console.log(name) 6 | switch (name) { 7 | case 'main': 8 | default: 9 | 10 | break 11 | } 12 | }) 13 | 14 | -------------------------------------------------------------------------------- /src/renderer/utils/message.js: -------------------------------------------------------------------------------- 1 | export const requestMsg = { 2 | fail: '请求异常😮,可以多试几次,若还是不行就换一首吧。。。', 3 | unachievable: '哦No😱...接口无法访问了!', 4 | // unachievable: '哦No😱...接口无法访问了!已帮你切换到临时接口,重试下看能不能播放吧~', 5 | notConnectNetwork: '无法连接到服务器', 6 | cancelRequest: '取消http请求', 7 | } 8 | -------------------------------------------------------------------------------- /src/main/events/clearCache.js: -------------------------------------------------------------------------------- 1 | const { mainHandle } = require('../../common/ipc') 2 | 3 | mainHandle('clearCache', async(event, options) => { 4 | if (!global.mainWindow) throw new Error('mainwindow is undefined') 5 | return global.mainWindow.webContents.session.clearCache() 6 | }) 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/main/events/getCacheSize.js: -------------------------------------------------------------------------------- 1 | const { mainHandle } = require('../../common/ipc') 2 | 3 | mainHandle('getCacheSize', async(event, options) => { 4 | if (!global.mainWindow) throw new Error('mainwindow is undefined') 5 | return global.mainWindow.webContents.session.getCacheSize() 6 | }) 7 | 8 | -------------------------------------------------------------------------------- /src/main/events/progressBar.js: -------------------------------------------------------------------------------- 1 | const { mainOn } = require('../../common/ipc') 2 | 3 | 4 | mainOn('progress', (event, params) => { 5 | // console.log(params) 6 | global.mainWindow && global.mainWindow.setProgressBar(params.status, { 7 | mode: params.mode || 'normal', 8 | }) 9 | }) 10 | 11 | -------------------------------------------------------------------------------- /publish/utils/packAssets.js: -------------------------------------------------------------------------------- 1 | const builder = require('electron-builder') 2 | const chalk = require('chalk') 3 | 4 | // Promise is returned 5 | module.exports = () => builder.build().catch(error => { 6 | console.log(error) 7 | console.log(chalk.red('Asset build failed.')) 8 | return Promise.reject(error) 9 | }) 10 | 11 | -------------------------------------------------------------------------------- /src/main/events/selectDir.js: -------------------------------------------------------------------------------- 1 | const { mainHandle } = require('../../common/ipc') 2 | const { dialog } = require('electron') 3 | 4 | mainHandle('selectDir', async(event, options) => { 5 | if (!global.mainWindow) throw new Error('mainwindow is undefined') 6 | return dialog.showOpenDialog(global.mainWindow, options) 7 | }) 8 | 9 | -------------------------------------------------------------------------------- /src/main/events/setIgnoreMouseEvent.js: -------------------------------------------------------------------------------- 1 | const { mainOn } = require('../../common/ipc') 2 | 3 | mainOn('setIgnoreMouseEvents', (event, isIgnored) => { 4 | if (!global.mainWindow) return 5 | isIgnored 6 | ? global.mainWindow.setIgnoreMouseEvents(true, { forward: true }) 7 | : global.mainWindow.setIgnoreMouseEvents(false) 8 | }) 9 | -------------------------------------------------------------------------------- /src/main/events/showSaveDialog.js: -------------------------------------------------------------------------------- 1 | const { mainHandle } = require('../../common/ipc') 2 | const { dialog } = require('electron') 3 | 4 | mainHandle('showSaveDialog', async(event, options) => { 5 | if (!global.mainWindow) throw new Error('mainwindow is undefined') 6 | return dialog.showSaveDialog(global.mainWindow, options) 7 | }) 8 | 9 | -------------------------------------------------------------------------------- /src/main/events/appName.js: -------------------------------------------------------------------------------- 1 | const { mainOn } = require('../../common/ipc') 2 | const { app } = require('electron') 3 | const { name: defaultName } = require('../../../package.json') 4 | 5 | 6 | mainOn('appName', (event, params) => { 7 | if (params == null) { 8 | app.setName(defaultName) 9 | } else { 10 | app.setName(params.name) 11 | } 12 | }) 13 | 14 | -------------------------------------------------------------------------------- /src/main/events/index.js: -------------------------------------------------------------------------------- 1 | 2 | require('./request') 3 | // require('./appName') 4 | require('./progressBar') 5 | require('./trafficLight') 6 | require('./musicMeta') 7 | require('./selectDir') 8 | require('./setWindowSize') 9 | require('./showSaveDialog') 10 | require('./clearCache') 11 | require('./getCacheSize') 12 | require('./setIgnoreMouseEvent') 13 | require('./getEnvParams') 14 | -------------------------------------------------------------------------------- /src/main/utils/musicMeta.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const mp3Meta = require('./mp3Meta') 3 | const flacMeta = require('./flacMeta') 4 | 5 | exports.setMeta = (filePath, meta) => { 6 | switch (path.extname(filePath)) { 7 | case '.mp3': 8 | mp3Meta(filePath, meta) 9 | break 10 | case '.flac': 11 | flacMeta(filePath, meta) 12 | break 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.appveyor.yml: -------------------------------------------------------------------------------- 1 | platform: 2 | - x64 3 | 4 | cache: 5 | - node_modules 6 | - '%APPDATA%\npm-cache' 7 | - '%LOCALAPPDATA%\electron\Cache' 8 | - '%LOCALAPPDATA%\electron-builder\Cache' 9 | 10 | install: 11 | - ps: Install-Product node 12 x64 12 | - npm install 13 | 14 | build_script: 15 | - npm run publish:gh 16 | 17 | test: off 18 | 19 | branches: 20 | only: 21 | - master 22 | -------------------------------------------------------------------------------- /src/common/error.js: -------------------------------------------------------------------------------- 1 | const { log } = require('./utils') 2 | 3 | process.on('uncaughtException', function(err) { 4 | console.error('An uncaught error occurred!') 5 | console.error(err) 6 | log.error(err) 7 | }) 8 | process.on('unhandledRejection', (reason, p) => { 9 | console.error('Unhandled Rejection at: Promise ', p) 10 | console.error(' reason: ', reason) 11 | log.error(reason) 12 | }) 13 | -------------------------------------------------------------------------------- /src/renderer/utils/music/kw/pic.js: -------------------------------------------------------------------------------- 1 | import { httpFetch } from '../../request' 2 | 3 | export default { 4 | getPic({ songmid }) { 5 | const requestObj = httpFetch(`http://artistpicserver.kuwo.cn/pic.web?corp=kuwo&type=rid_pic&pictype=500&size=500&rid=${songmid}`) 6 | requestObj.promise = requestObj.promise.then(({ body }) => /^http/.test(body) ? body : null) 7 | return requestObj 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "corejs": "3", 7 | "useBuiltIns": "usage" 8 | } 9 | ], 10 | [ 11 | "minify", 12 | { 13 | "builtIns": false, 14 | "evaluate": false, 15 | "mangle": false 16 | } 17 | ] 18 | ], 19 | "plugins": [ 20 | "@babel/plugin-syntax-dynamic-import" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /src/common/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | windowSizeList: [ 3 | { 4 | id: 1, 5 | name: '小', 6 | width: 920, 7 | height: 590, 8 | tabList: '645px', 9 | }, 10 | { 11 | id: 2, 12 | name: '中', 13 | width: 1012, 14 | height: 650, 15 | tabList: '719px', 16 | }, 17 | { 18 | id: 3, 19 | name: '大', 20 | width: 1104, 21 | height: 708, 22 | tabList: '792px', 23 | }, 24 | ], 25 | } 26 | -------------------------------------------------------------------------------- /src/renderer/utils/music/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 获取音乐音质 3 | * @param {*} info 4 | * @param {*} type 5 | */ 6 | 7 | const types = ['flac', 'ape', '320k', '192k', '128k'] 8 | export const getMusicType = (info, type) => { 9 | switch (info.source) { 10 | // case 'kg': 11 | case 'wy': 12 | case 'tx': 13 | return '128k' 14 | } 15 | const rangeType = types.slice(types.indexOf(type)) 16 | for (const type of rangeType) { 17 | if (info._types[type]) return type 18 | } 19 | return '128k' 20 | } 21 | -------------------------------------------------------------------------------- /publish/changeLog.md: -------------------------------------------------------------------------------- 1 | ### 新增 2 | 3 | - 允许选中列表内歌曲名、歌手名、专辑名内的文字,选中后可使用键盘快捷键进行复制 4 | - 新增在列表可选内容区域**鼠标右击**时自动复制列表已选文字的功能 5 | - 新增在搜索框**鼠标右击**时自动粘贴剪贴板的文本到搜索框中 6 | - 任务下载失败时将显示搜索按钮,方便在其他源搜索该歌曲 7 | 8 | ### 优化 9 | 10 | - 优化木叶之村主题翻页器背景颜色 11 | - 优化各个主题音质标签颜色 12 | - 优化其他一些界面细节及用户交互效果 13 | 14 | ### 修复 15 | 16 | - 修复启用透明窗口鼠标不穿透的bug 17 | - 修复大窗口时设置的音乐来源选项不换行的问题 18 | - 修复某些情况下暂停任务会自动开始任务的问题 19 | - 修复移除暂停、错误的任务时不删除未下载完成的文件的问题 20 | - 修复酷狗源歌单热门标签歌单列表无法加载问题 21 | - 修复QQ源歌单热门标签歌单列表无法加载问题 22 | 23 | ### 其他 24 | 25 | - 更新electron到 8.0.1 26 | -------------------------------------------------------------------------------- /src/renderer/components/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import upperFirst from 'lodash/upperFirst' 3 | import camelCase from 'lodash/camelCase' 4 | 5 | const requireComponent = require.context('./', true, /\.vue$/) 6 | 7 | requireComponent.keys().forEach(fileName => { 8 | const componentConfig = requireComponent(fileName) 9 | 10 | const componentName = upperFirst(camelCase(fileName.replace(/^\.\//, '').replace(/\.\w+$/, ''))) 11 | 12 | Vue.component(componentName, componentConfig.default || componentConfig) 13 | }) 14 | -------------------------------------------------------------------------------- /src/renderer/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import { sync } from 'vuex-router-sync' 3 | 4 | // Components 5 | import './components' 6 | 7 | // Plugins 8 | import './plugins' 9 | 10 | import App from './App' 11 | import router from './route' 12 | import store from './store' 13 | 14 | import '../common/error' 15 | 16 | sync(store, router) 17 | 18 | if (!process.env.IS_WEB) { 19 | 20 | } 21 | 22 | Vue.config.productionTip = false 23 | 24 | new Vue({ 25 | router, 26 | store, 27 | el: '#root', 28 | render: h => h(App), 29 | }) 30 | -------------------------------------------------------------------------------- /src/main/events/trafficLight.js: -------------------------------------------------------------------------------- 1 | // const { app } = require('electron') 2 | const { mainOn } = require('../../common/ipc') 3 | 4 | 5 | mainOn('min', event => { 6 | if (global.mainWindow) { 7 | global.mainWindow.minimize() 8 | } 9 | }) 10 | mainOn('max', event => { 11 | if (global.mainWindow) { 12 | global.mainWindow.maximize() 13 | } 14 | }) 15 | mainOn('close', event => { 16 | if (global.mainWindow) { 17 | // global.mainWindowdow.destroy() 18 | // console.log('close') 19 | // app.quit() 20 | global.mainWindow.close() 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/----.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 功能请求(请先查看常见问题及搜索issue列表中有无你要提的问题) 3 | about: 为这个项目提出一个想法 4 | title: 例如:添加xxx功能、优化xxx功能 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **解决方案检查** 11 | 12 | [] 我已阅读常见问题() 13 | [] 我已搜索issue列表() 14 | 15 | **描述您想要的解决方案** 16 | 简洁明了地描述您要发生的事情。 17 | 18 | **描述您考虑过的替代方案** 19 | 对您考虑过的所有替代解决方案或功能的简洁明了的描述。 20 | 21 | **其他内容** 22 | 在此处添加有关功能请求的任何其他上下文或屏幕截图(直接把图片拖到编辑框即可添加图片)。 23 | -------------------------------------------------------------------------------- /src/renderer/components/core/View.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 26 | -------------------------------------------------------------------------------- /src/renderer/utils/music/mg/index.js: -------------------------------------------------------------------------------- 1 | import api_source from '../api-source' 2 | import leaderboard from './leaderboard' 3 | import songList from './songList' 4 | import musicSearch from './musicSearch' 5 | import pic from './pic' 6 | import lyric from './lyric' 7 | 8 | const mg = { 9 | songList, 10 | musicSearch, 11 | leaderboard, 12 | getMusicUrl(songInfo, type) { 13 | return api_source('mg').getMusicUrl(songInfo, type) 14 | }, 15 | getLyric(songInfo) { 16 | return lyric.getLyric(songInfo) 17 | }, 18 | getPic(songInfo) { 19 | return pic.getPic(songInfo) 20 | }, 21 | } 22 | 23 | export default mg 24 | -------------------------------------------------------------------------------- /src/renderer/utils/music/kw/api-temp.js: -------------------------------------------------------------------------------- 1 | import { httpFetch } from '../../request' 2 | import { headers, timeout } from '../options' 3 | 4 | const api_temp = { 5 | getMusicUrl(songInfo, type) { 6 | const requestObj = httpFetch(`http://tm.tempmusic.tk/url/kw/${songInfo.songmid}/${type}`, { 7 | method: 'get', 8 | headers, 9 | timeout, 10 | family: 4, 11 | }) 12 | requestObj.promise = requestObj.promise.then(({ body }) => { 13 | return body.code === 0 ? Promise.resolve({ type, url: body.data }) : Promise.reject(new Error(body.msg)) 14 | }) 15 | return requestObj 16 | }, 17 | } 18 | 19 | export default api_temp 20 | -------------------------------------------------------------------------------- /src/index.pug: -------------------------------------------------------------------------------- 1 | html(lang="cn") 2 | head 3 | meta(charset="UTF-8") 4 | meta(name="viewport" content="width=device-width, initial-scale=1.0") 5 | meta(http-equiv="X-UA-Compatible" content="ie=edge") 6 | title 洛雪音乐助手 7 | 8 | body 9 | #root 10 | //- if htmlWebpackPlugin.options.isProd 11 | //- script. 12 | //- window.__static = '!{require('path').join(htmlWebpackPlugin.options.__dirname, '/static').replace(/\\/g, '\\\\')}' 13 | //- if !htmlWebpackPlugin.options.browser && htmlWebpackPlugin.options.isProd 14 | script. 15 | window.__static = require('path').join(__dirname, '/resources').replace(/\\/g, '\\\\') 16 | -------------------------------------------------------------------------------- /src/renderer/utils/music/wy/index.js: -------------------------------------------------------------------------------- 1 | import leaderboard from './leaderboard' 2 | import api_source from '../api-source' 3 | import getLyric from './lyric' 4 | import getMusicInfo from './musicInfo' 5 | import musicSearch from './musicSearch' 6 | import songList from './songList' 7 | 8 | const wy = { 9 | leaderboard, 10 | musicSearch, 11 | songList, 12 | getMusicUrl(songInfo, type) { 13 | return api_source('wy').getMusicUrl(songInfo, type) 14 | }, 15 | getLyric(songInfo) { 16 | return getLyric(songInfo.songmid) 17 | }, 18 | getPic(songInfo) { 19 | return getMusicInfo(songInfo.songmid).then(info => info.al.picUrl) 20 | }, 21 | } 22 | 23 | export default wy 24 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "plugins": [ 4 | "html" 5 | ], 6 | "parser": "babel-eslint", 7 | "rules": { 8 | "no-new": "off", 9 | "camelcase": "off", 10 | "no-return-assign": "off", 11 | "space-before-function-paren": ["error", "never"], 12 | "no-var": "error", 13 | "no-fallthrough": "off", 14 | "prefer-promise-reject-errors": "off", 15 | "eqeqeq": "off", 16 | "no-multiple-empty-lines": [1, {"max": 2}], 17 | "comma-dangle": [2, "always-multiline"], 18 | "standard/no-callback-literal": "off", 19 | "prefer-const": "off" 20 | }, 21 | "settings": { 22 | "html/html-extensions": [".html", ".vue"] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /publish/utils/compileAssets.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require('child_process') 2 | const { jp } = require('./index') 3 | const chalk = require('chalk') 4 | 5 | module.exports = () => new Promise((resolve, reject) => { 6 | const pack = spawn('node', [jp('../../build-config/pack.js')]) 7 | 8 | // pack.stdout.on('data', (data) => { 9 | // console.log(chalk.blue(data)) 10 | // }) 11 | 12 | pack.stderr.on('data', (data) => { 13 | console.log(chalk.red(data)) 14 | }) 15 | 16 | pack.on('close', code => { 17 | if (code === 0) { 18 | resolve() 19 | } else { 20 | console.log(chalk.red('Asset compilation failed.')) 21 | reject() 22 | } 23 | }) 24 | }) 25 | 26 | -------------------------------------------------------------------------------- /src/renderer/utils/music/bd/api-test.js: -------------------------------------------------------------------------------- 1 | import { httpFetch } from '../../request' 2 | import { requestMsg } from '../../message' 3 | import { headers, timeout } from '../options' 4 | 5 | const api_test = { 6 | getMusicUrl(songInfo, type) { 7 | const requestObj = httpFetch(`http://ts.tempmusic.tk/url/bd/${songInfo.songmid}/${type}`, { 8 | method: 'get', 9 | timeout, 10 | headers, 11 | family: 4, 12 | }) 13 | requestObj.promise = requestObj.promise.then(({ body }) => { 14 | return body.code === 0 ? Promise.resolve({ type, url: body.data }) : Promise.reject(new Error(requestMsg.fail)) 15 | }) 16 | return requestObj 17 | }, 18 | } 19 | 20 | export default api_test 21 | -------------------------------------------------------------------------------- /src/renderer/utils/music/mg/api-test.js: -------------------------------------------------------------------------------- 1 | import { httpFetch } from '../../request' 2 | import { requestMsg } from '../../message' 3 | import { headers, timeout } from '../options' 4 | 5 | const api_test = { 6 | getMusicUrl(songInfo, type) { 7 | const requestObj = httpFetch(`http://ts.tempmusic.tk/url/mg/${songInfo.copyrightId}/${type}`, { 8 | method: 'get', 9 | timeout, 10 | headers, 11 | family: 4, 12 | }) 13 | requestObj.promise = requestObj.promise.then(({ body }) => { 14 | return body.code === 0 ? Promise.resolve({ type, url: body.data }) : Promise.reject(new Error(requestMsg.fail)) 15 | }) 16 | return requestObj 17 | }, 18 | } 19 | 20 | export default api_test 21 | -------------------------------------------------------------------------------- /src/renderer/utils/music/tx/index.js: -------------------------------------------------------------------------------- 1 | import leaderboard from './leaderboard' 2 | import lyric from './lyric' 3 | import songList from './songList' 4 | import musicSearch from './musicSearch' 5 | import api_source from '../api-source' 6 | 7 | const tx = { 8 | leaderboard, 9 | songList, 10 | musicSearch, 11 | 12 | getMusicUrl(songInfo, type) { 13 | return api_source('tx').getMusicUrl(songInfo, type) 14 | }, 15 | getLyric(songInfo) { 16 | // let singer = songInfo.singer.indexOf('、') > -1 ? songInfo.singer.split('、')[0] : songInfo.singer 17 | return lyric.getLyric(songInfo.songmid) 18 | }, 19 | getPic(songInfo) { 20 | return api_source('tx').getPic(songInfo) 21 | }, 22 | } 23 | 24 | export default tx 25 | -------------------------------------------------------------------------------- /src/renderer/assets/styles/layout.less: -------------------------------------------------------------------------------- 1 | @import './variables.less'; 2 | 3 | 4 | /*自动隐藏文字*/ 5 | .mixin-ellipsis-1() { 6 | overflow: hidden; 7 | white-space: nowrap; 8 | text-overflow: ellipsis; 9 | } 10 | .mixin-ellipsis() { 11 | display: -webkit-box; 12 | overflow: hidden; 13 | text-overflow: ellipsis; 14 | word-wrap: break-word; 15 | word-break: break-all; 16 | white-space: normal !important; 17 | -webkit-line-clamp: 1; 18 | -webkit-box-orient: vertical; 19 | } 20 | .mixin-ellipsis-2() { 21 | display: -webkit-box; 22 | overflow: hidden; 23 | text-overflow: ellipsis; 24 | word-wrap: break-word; 25 | word-break: break-all; 26 | white-space: normal !important; 27 | -webkit-line-clamp: 2; 28 | -webkit-box-orient: vertical; 29 | } 30 | 31 | -------------------------------------------------------------------------------- /src/renderer/store/modules/index.js: -------------------------------------------------------------------------------- 1 | // https://vuex.vuejs.org/en/modules.html 2 | 3 | const requireModule = require.context('./', true, /\.js$/) 4 | const modules = {} 5 | 6 | requireModule.keys().forEach(fileName => { 7 | if (fileName === './index.js') return 8 | const path = fileName.replace(/(\.\/|\.js)/g, '') 9 | 10 | if (/\//.test(path)) { 11 | // Replace ./ and .js 12 | const [moduleName, imported] = path.split('/') 13 | 14 | if (!modules[moduleName]) { 15 | modules[moduleName] = { 16 | namespaced: true, 17 | } 18 | } 19 | 20 | modules[moduleName][imported] = requireModule(fileName).default 21 | } else { 22 | modules[path] = requireModule(fileName).default 23 | } 24 | }) 25 | 26 | export default modules 27 | -------------------------------------------------------------------------------- /src/renderer/utils/music/tx/lyric.js: -------------------------------------------------------------------------------- 1 | import { httpFetch } from '../../request' 2 | import { b64DecodeUnicode } from '../../index' 3 | 4 | export default { 5 | regexps: { 6 | matchLrc: /.+"lyric":"([\w=+/]*)".+/, 7 | }, 8 | getLyric(songmid) { 9 | const requestObj = httpFetch(`https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg?songmid=${songmid}&g_tk=2001461048&loginUin=0&hostUin=0&format=jsonp&inCharset=utf8&outCharset=utf-8&platform=yqq`, { 10 | headers: { 11 | Referer: 'https://y.qq.com/portal/player.html', 12 | }, 13 | }) 14 | requestObj.promise = requestObj.promise.then(({ body }) => { 15 | return b64DecodeUnicode(body.replace(this.regexps.matchLrc, '$1')) 16 | }) 17 | return requestObj 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/--bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 报告Bug(请先查看常见问题及搜索issue列表中有无你要提的问题) 3 | about: 创建报告以帮助我们改进 4 | title: 例如:音乐无法播放 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **解决方案检查** 11 | 12 | [] 我已阅读常见问题() 13 | [] 我已搜索issue列表() 14 | 15 | **描述错误** 16 | 清楚简洁地说明错误是什么。 17 | 18 | **重现** 19 | 重现行为的步骤: 20 | 1.转到“ ...” 21 | 2.点击“ ....” 22 | 3.向下滚动到“ ....” 23 | 4.看到错误 24 | 25 | **预期行为** 26 | 对您期望发生的事情的简洁明了的描述。 27 | 28 | **截图** 29 | 如果适用,请添加屏幕截图以帮助解释您的问题(直接把图片拖到编辑框即可添加图片)。 30 | 31 | **环境:** 32 |   -操作系统及版本:[例如:Windows 10 64位 18362.156] 33 |   -软件安装包及版本:[例如:Windows 64位绿色版 1.0.0] 34 | 35 | **其他内容** 36 | 在此处添加有关该问题的任何其他上下文。 37 | -------------------------------------------------------------------------------- /src/main/utils/index.js: -------------------------------------------------------------------------------- 1 | const Store = require('electron-store') 2 | const { windowSizeList } = require('../../common/config') 3 | 4 | exports.getWindowSizeInfo = () => { 5 | let electronStore = new Store() 6 | const { windowSizeId = 1 } = electronStore.get('setting') || {} 7 | return windowSizeList.find(i => i.id === windowSizeId) || windowSizeList[0] 8 | } 9 | 10 | exports.parseEnv = () => { 11 | const params = {} 12 | const rx = /^-\w+/ 13 | for (let param of process.argv) { 14 | if (!rx.test(param)) continue 15 | param = param.substring(1) 16 | let index = param.indexOf('=') 17 | if (index < 0) { 18 | params[param] = true 19 | } else { 20 | params[param.substring(0, index)] = param.substring(index + 1) 21 | } 22 | } 23 | return params 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/renderer/utils/music/kg/index.js: -------------------------------------------------------------------------------- 1 | import leaderboard from './leaderboard' 2 | import api_source from '../api-source' 3 | import songList from './songList' 4 | import musicSearch from './musicSearch' 5 | import pic from './pic' 6 | import lyric from './lyric' 7 | 8 | const kg = { 9 | leaderboard, 10 | songList, 11 | musicSearch, 12 | getMusicUrl(songInfo, type) { 13 | return api_source('kg').getMusicUrl(songInfo, type) 14 | }, 15 | getLyric(songInfo) { 16 | return lyric.getLyric(songInfo) 17 | }, 18 | // getLyric(songInfo) { 19 | // return api_source('kg').getLyric(songInfo) 20 | // }, 21 | getPic(songInfo) { 22 | return pic.getPic(songInfo) 23 | }, 24 | // getPic(songInfo) { 25 | // return api_source('kg').getPic(songInfo) 26 | // }, 27 | } 28 | 29 | export default kg 30 | -------------------------------------------------------------------------------- /src/renderer/utils/music/kw/lyric.js: -------------------------------------------------------------------------------- 1 | import { httpFetch } from '../../request' 2 | 3 | export default { 4 | formatTime(time) { 5 | let m = parseInt(time / 60) 6 | let s = (time % 60).toFixed(2) 7 | return (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s) 8 | }, 9 | transformLrc({ songinfo, lrclist }) { 10 | return `[ti:${songinfo.songName}]\n[ar:${songinfo.artist}]\n[al:${songinfo.album}]\n[by:]\n[offset:0]\n${lrclist ? lrclist.map(l => `[${this.formatTime(l.time)}]${l.lineLyric}\n`).join('') : '暂无歌词'}` 11 | }, 12 | getLyric(songId) { 13 | const requestObj = httpFetch(`http://m.kuwo.cn/newh5/singles/songinfoandlrc?musicId=${songId}`) 14 | requestObj.promise = requestObj.promise.then(({ body }) => { 15 | return this.transformLrc(body.data) 16 | }) 17 | return requestObj 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /src/renderer/utils/music/wy/lyric.js: -------------------------------------------------------------------------------- 1 | import { httpFetch } from '../../request' 2 | import { weapi } from './utils/crypto' 3 | 4 | export default songmid => { 5 | const requestObj = httpFetch('http://music.163.com/weapi/song/lyric?csrf_token=', { 6 | method: 'post', 7 | headers: { 8 | 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36', 9 | Referer: 'https://music.163.com/song?id=' + songmid, 10 | origin: 'https://music.163.com', 11 | }, 12 | form: weapi({ id: songmid, lv: -1, tv: -1, csrf_token: '' }), 13 | }) 14 | requestObj.promise = requestObj.promise.then(({ body }) => { 15 | // console.log(body) 16 | if (body.code !== 200) return Promise.reject('获取歌词失败') 17 | return body.lrc.lyric 18 | }) 19 | return requestObj 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/renderer/utils/music/bd/musicInfo.js: -------------------------------------------------------------------------------- 1 | import { httpFetch } from '../../request' 2 | 3 | export default { 4 | cache: {}, 5 | getMusicInfo(songmid) { 6 | if (this.cache[songmid]) { 7 | return { promise: Promise.resolve(this.cache[songmid]) } 8 | } 9 | const requestObj = httpFetch(`https://musicapi.qianqian.com/v1/restserver/ting?method=baidu.ting.song.getSongLink&format=json&from=bmpc&version=1.0.0&version_d=11.1.6.0&songid=${songmid}&type=1&res=1&s_protocol=1&aac=2&project=tpass`) 10 | requestObj.promise = requestObj.promise.then(({ body }) => { 11 | // console.log(body) 12 | if (body.error_code == 22000) { 13 | this.cache[songmid] = body.result.songinfo 14 | return body.result.songinfo 15 | } 16 | return Promise.reject(new Error('获取音乐信息失败')) 17 | }) 18 | return requestObj 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /src/renderer/utils/music/mg/pic.js: -------------------------------------------------------------------------------- 1 | import { httpFetch } from '../../request' 2 | 3 | export default { 4 | getPic(songInfo, tryNum = 0) { 5 | let requestObj = httpFetch(`http://music.migu.cn/v3/api/music/audioPlayer/getSongPic?songId=${songInfo.songmid}`, { 6 | headers: { 7 | Referer: 'http://music.migu.cn/v3/music/player/audio?from=migu', 8 | }, 9 | }) 10 | requestObj.promise = requestObj.promise.then(({ body }) => { 11 | if (body.returnCode !== '000000') { 12 | if (tryNum > 5) return Promise.reject('图片获取失败') 13 | let tryRequestObj = this.getPic(songInfo, ++tryNum) 14 | requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj) 15 | return tryRequestObj.promise 16 | } 17 | return body.largePic || body.mediumPic || body.smallPic 18 | }) 19 | return requestObj 20 | }, 21 | } 22 | -------------------------------------------------------------------------------- /src/renderer/route/paths.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | // { 3 | // path: '/', 4 | // // redirect: '/app', 5 | // // props: true, 6 | // component: () => import('../views/Dashboard.vue'), 7 | // name: 'Dashboard', 8 | // alias: '/dashboard' 9 | // } 10 | { 11 | path: '/search', 12 | name: 'search', 13 | view: 'Search', 14 | }, 15 | { 16 | path: '/leaderboard', 17 | name: 'leaderboard', 18 | view: 'Leaderboard', 19 | }, 20 | { 21 | path: '/songList', 22 | name: 'songList', 23 | view: 'SongList', 24 | }, 25 | { 26 | path: '/list', 27 | name: 'list', 28 | view: 'List', 29 | // props: true, 30 | }, 31 | { 32 | path: '/download', 33 | name: 'download', 34 | view: 'Download', 35 | }, 36 | { 37 | path: '/setting', 38 | name: 'setting', 39 | view: 'Setting', 40 | }, 41 | ] 42 | -------------------------------------------------------------------------------- /src/renderer/utils/music/tx/api-test.js: -------------------------------------------------------------------------------- 1 | import { httpFetch } from '../../request' 2 | import { requestMsg } from '../../message' 3 | import { headers, timeout } from '../options' 4 | 5 | const api_messoer = { 6 | getMusicUrl(songInfo, type) { 7 | const requestObj = httpFetch(`http://ts.tempmusic.tk/url/tx/${songInfo.songmid}/${type}`, { 8 | method: 'get', 9 | timeout, 10 | headers, 11 | family: 4, 12 | }) 13 | requestObj.promise = requestObj.promise.then(({ body }) => { 14 | return body.code === 0 ? Promise.resolve({ type, url: body.data }) : Promise.reject(new Error(requestMsg.fail)) 15 | }) 16 | return requestObj 17 | }, 18 | getPic(songInfo) { 19 | return { 20 | promise: Promise.resolve(`https://y.gtimg.cn/music/photo_new/T002R500x500M000${songInfo.albumId}.jpg`), 21 | } 22 | }, 23 | } 24 | 25 | export default api_messoer 26 | -------------------------------------------------------------------------------- /src/common/ipc.js: -------------------------------------------------------------------------------- 1 | const { ipcMain, ipcRenderer } = require('electron') 2 | 3 | 4 | exports.mainOn = (event, callback) => { 5 | ipcMain.on(event, callback) 6 | } 7 | exports.mainOnce = (event, callback) => { 8 | ipcMain.once(event, callback) 9 | } 10 | 11 | exports.mainHandle = (name, callback) => { 12 | ipcMain.handle(name, callback) 13 | } 14 | exports.mainHandleOnce = (name, callback) => { 15 | ipcMain.handleOnce(name, callback) 16 | } 17 | 18 | 19 | exports.rendererSend = (name, params) => { 20 | ipcRenderer.send(name, params) 21 | } 22 | exports.rendererSendSync = (name, params) => ipcRenderer.sendSync(name, params) 23 | 24 | exports.rendererInvoke = (name, params) => ipcRenderer.invoke(name, params) 25 | 26 | exports.rendererOn = (name, callback) => { 27 | ipcRenderer.on(name, callback) 28 | } 29 | exports.rendererOnce = (name, callback) => { 30 | ipcRenderer.once(name, callback) 31 | } 32 | -------------------------------------------------------------------------------- /src/renderer/utils/music/index.js: -------------------------------------------------------------------------------- 1 | import kw from './kw' 2 | import kg from './kg' 3 | import tx from './tx' 4 | import wy from './wy' 5 | import mg from './mg' 6 | import bd from './bd' 7 | const sources = { 8 | sources: [ 9 | { 10 | name: '酷我音乐', 11 | id: 'kw', 12 | }, 13 | { 14 | name: '酷狗音乐', 15 | id: 'kg', 16 | }, 17 | { 18 | name: 'QQ音乐', 19 | id: 'tx', 20 | }, 21 | { 22 | name: '网易音乐', 23 | id: 'wy', 24 | }, 25 | { 26 | name: '咪咕音乐', 27 | id: 'mg', 28 | }, 29 | { 30 | name: '百度音乐', 31 | id: 'bd', 32 | }, 33 | ], 34 | kw, 35 | kg, 36 | tx, 37 | wy, 38 | mg, 39 | bd, 40 | } 41 | export default { 42 | ...sources, 43 | init() { 44 | for (let source of sources.sources) { 45 | let sm = sources[source.id] 46 | sm && sm.init && sm.init() 47 | } 48 | }, 49 | } 50 | -------------------------------------------------------------------------------- /src/main/index.dev.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is used specifically and only for development. It installs 3 | * `electron-debug` & `vue-devtools`. There shouldn't be any need to 4 | * modify this file, but it can be used to extend your development 5 | * environment. 6 | */ 7 | const electron = require('electron') 8 | const electronDebug = require('electron-debug') 9 | const { default: installExtension, VUEJS_DEVTOOLS } = require('electron-devtools-installer') 10 | // Install `electron-debug` with `devtron` 11 | electronDebug({ 12 | showDevTools: true, 13 | devToolsMode: 'undocked', 14 | }) 15 | 16 | // Install `vue-devtools` 17 | electron.app.on('ready', () => { 18 | installExtension(VUEJS_DEVTOOLS) 19 | .then(name => console.log(`Added Extension: ${name}`)) 20 | .catch(err => console.log('An error occurred: ', err)) 21 | }) 22 | 23 | // Require `main` process to boot app 24 | require('./index') 25 | 26 | -------------------------------------------------------------------------------- /src/renderer/utils/music/tx/api-internal.js: -------------------------------------------------------------------------------- 1 | import { httpFatch } from '../../request' 2 | import { requestMsg } from '../../message' 3 | import { headers, timeout } from '../options' 4 | 5 | const api_messoer = { 6 | getMusicUrl(songInfo, type) { 7 | const requestObj = httpFatch(`https://v1.itooi.cn/tencent/url?id=${songInfo.strMediaMid}&quality=${type.replace(/k$/, '')}&isRedirect=0`, { 8 | method: 'get', 9 | timeout, 10 | headers, 11 | }) 12 | requestObj.promise = requestObj.promise.then(({ body }) => { 13 | return body.code === 200 ? Promise.resolve({ type, url: body.data }) : Promise.reject(new Error(requestMsg.fail)) 14 | }) 15 | return requestObj 16 | }, 17 | getPic(songInfo) { 18 | return { 19 | promise: Promise.resolve(`https://y.gtimg.cn/music/photo_new/T002R500x500M000${songInfo.albumId}.jpg`), 20 | } 21 | }, 22 | } 23 | 24 | export default api_messoer 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: true 2 | language: node_js 3 | node_js: 12 4 | 5 | matrix: 6 | include: 7 | - os: osx 8 | osx_image: xcode10.2 9 | env: 10 | - ELECTRON_CACHE=$HOME/.cache/electron 11 | - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder 12 | 13 | - os: linux 14 | dist: trusty 15 | 16 | cache: 17 | directories: 18 | - node_modules 19 | - $HOME/.cache/electron 20 | - $HOME/.cache/electron-builder 21 | - $HOME/.npm/_prebuilds 22 | 23 | notifications: 24 | email: false 25 | 26 | script: 27 | - node --version 28 | - npm --version 29 | - | 30 | if [ "$TRAVIS_OS_NAME" == "linux" ]; then 31 | npm install && npm run publish:gh:linux 32 | else 33 | npm run publish:gh:mac 34 | fi 35 | 36 | before_cache: 37 | - rm -rf $HOME/.cache/electron-builder/wine 38 | 39 | # only run this script on pull requests and merges into 40 | # the 'master' and 'prod' branches 41 | branches: 42 | only: 43 | - master 44 | -------------------------------------------------------------------------------- /src/renderer/utils/music/wy/musicInfo.js: -------------------------------------------------------------------------------- 1 | // https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/module/song_detail.js 2 | import { httpFetch } from '../../request' 3 | import { weapi } from './utils/crypto' 4 | 5 | export default songmid => { 6 | const requestObj = httpFetch('https://music.163.com/weapi/v3/song/detail', { 7 | method: 'post', 8 | headers: { 9 | 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36', 10 | Referer: 'https://music.163.com/song?id=' + songmid, 11 | origin: 'https://music.163.com', 12 | }, 13 | form: weapi({ 14 | c: `[{"id":${songmid}}]`, 15 | ids: `[${songmid}]`, 16 | }), 17 | }) 18 | requestObj.promise = requestObj.promise.then(({ body }) => { 19 | // console.log(body) 20 | if (body.code !== 200 || !body.songs.length) return Promise.reject('获取歌曲信息失败') 21 | return body.songs[0] 22 | }) 23 | return requestObj 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/renderer/store/getters.js: -------------------------------------------------------------------------------- 1 | import music from '../utils/music' 2 | 3 | export default { 4 | theme(state) { 5 | return (state.themes[state.setting.themeId] && state.themes[state.setting.themeId].class) || '' 6 | }, 7 | themes(state) { 8 | return { 9 | active: state.setting.themeId, 10 | list: state.themes, 11 | } 12 | }, 13 | source(state) { 14 | return music.sources.find(s => s.id === state.setting.sourceId) || music.sources[0] 15 | }, 16 | sources(state) { 17 | return { 18 | active: state.setting.sourceId, 19 | list: music.sources, 20 | } 21 | }, 22 | userInfo(state) { 23 | return state.userInfo 24 | }, 25 | setting(state) { 26 | return state.setting 27 | }, 28 | settingVersion(state) { 29 | return state.settingVersion 30 | }, 31 | version(state) { 32 | return state.version 33 | }, 34 | route(state) { 35 | return state.route 36 | }, 37 | windowSizeList(state) { 38 | return state.windowSizeList 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /publish/utils/copyFile.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const chalk = require('chalk') 3 | const { jp, copyFile } = require('./index') 4 | 5 | const buildDir = '../../build' 6 | 7 | 8 | const getBuildFileName = () => { 9 | const names = [] 10 | const pathRegExp = [ 11 | /latest\.yml$/, 12 | /\.exe$/, 13 | /\.blockmap$/, 14 | ] 15 | const files = fs.readdirSync(jp(buildDir), 'utf8') 16 | files.forEach(name => { 17 | pathRegExp.forEach(regexp => { 18 | if (regexp.test(name)) names.push(name) 19 | }) 20 | }) 21 | return names 22 | } 23 | 24 | const copy = names => { 25 | const tasks = names.map(name => copyFile(jp(buildDir, name), jp('../assets', name))) 26 | return Promise.all(tasks) 27 | } 28 | 29 | 30 | module.exports = (isCopyVersion = true) => { 31 | copy(getBuildFileName()).then(() => { 32 | if (isCopyVersion) fs.writeFileSync(jp('../assets/version.json'), JSON.stringify(require('../version.json')), 'utf8') 33 | }).catch(err => { 34 | console.log(err) 35 | console.log(chalk.red('File copy failed.')) 36 | return Promise.reject(err) 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /src/renderer/utils/music/kw/util.js: -------------------------------------------------------------------------------- 1 | import { httpGet } from '../../request' 2 | 3 | if (!window.kw_token) { 4 | window.kw_token = { 5 | token: null, 6 | isGetingToken: false, 7 | } 8 | } 9 | 10 | export const formatSinger = rawData => rawData.replace(/&/g, '、') 11 | 12 | export const matchToken = headers => { 13 | try { 14 | return headers['set-cookie'][0].match(/kw_token=(\w+)/)[1] 15 | } catch (err) { 16 | return null 17 | } 18 | } 19 | 20 | const wait = time => new Promise(resolve => setTimeout(() => resolve(), time)) 21 | 22 | 23 | export const getToken = () => new Promise((resolve, reject) => { 24 | if (window.kw_token.isGetingToken) return wait(1000).then(() => getToken().then(token => resolve(token))) 25 | if (window.kw_token.token) return resolve(window.kw_token.token) 26 | window.kw_token.isGetingToken = true 27 | httpGet('http://www.kuwo.cn/', (err, resp) => { 28 | window.kw_token.isGetingToken = false 29 | if (err) return reject(err) 30 | if (resp.statusCode != 200) return reject(new Error('获取失败')) 31 | const token = window.kw_token.token = matchToken(resp.headers) 32 | resolve(token) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /src/renderer/utils/music/kw/api-internal.js: -------------------------------------------------------------------------------- 1 | import { httpFatch } from '../../request' 2 | import { requestMsg } from '../../message' 3 | import { headers, timeout } from '../options' 4 | 5 | const api_messoer = { 6 | getMusicUrl(songInfo, type) { 7 | const requestObj = httpFatch(`https://v1.itooi.cn/kuwo/url?id=${songInfo.songmid}&quality=${type.replace(/k$/, '')}&isRedirect=0`, { 8 | method: 'get', 9 | timeout, 10 | headers, 11 | }) 12 | requestObj.promise = requestObj.promise.then(({ body }) => { 13 | return body.code === 200 ? Promise.resolve({ type, url: body.data }) : Promise.reject(new Error(requestMsg.fail)) 14 | }) 15 | return requestObj 16 | }, 17 | getPic(songInfo) { 18 | const requestObj = httpFatch(`https://v1.itooi.cn/kuwo/pic?id=${songInfo.songmid}&isRedirect=0`, { 19 | method: 'get', 20 | timeout, 21 | headers, 22 | }) 23 | requestObj.promise = requestObj.promise.then(({ body }) => { 24 | return body.code === 200 ? Promise.resolve(body.data) : Promise.reject(new Error(requestMsg.fail)) 25 | }) 26 | return requestObj 27 | }, 28 | } 29 | 30 | export default api_messoer 31 | -------------------------------------------------------------------------------- /src/renderer/utils/music/kw/api-test.js: -------------------------------------------------------------------------------- 1 | import { httpFetch } from '../../request' 2 | import { requestMsg } from '../../message' 3 | import { headers, timeout } from '../options' 4 | 5 | const api_test = { 6 | // getMusicUrl(songInfo, type) { 7 | // const requestObj = httpFetch(`http://45.32.53.128:3002/m/kw/u/${songInfo.songmid}/${type}`, { 8 | // method: 'get', 9 | // headers, 10 | // timeout, 11 | // }) 12 | // requestObj.promise = requestObj.promise.then(({ body }) => { 13 | // return body.code === 0 ? Promise.resolve({ type, url: body.data }) : Promise.reject(new Error(body.msg)) 14 | // }) 15 | // return requestObj 16 | // }, 17 | getMusicUrl(songInfo, type) { 18 | const requestObj = httpFetch(`http://ts.tempmusic.tk/url/kw/${songInfo.songmid}/${type}`, { 19 | method: 'get', 20 | timeout, 21 | headers, 22 | family: 4, 23 | }) 24 | requestObj.promise = requestObj.promise.then(({ body }) => { 25 | return body.code === 0 ? Promise.resolve({ type, url: body.data }) : Promise.reject(new Error(requestMsg.fail)) 26 | }) 27 | return requestObj 28 | }, 29 | } 30 | 31 | export default api_test 32 | -------------------------------------------------------------------------------- /src/renderer/assets/styles/reset.less: -------------------------------------------------------------------------------- 1 | html, body, div, span, applet, object, iframe, 2 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 3 | a, abbr, acronym, address, big, cite, code, 4 | del, dfn, em, img, ins, kbd, q, s, samp, 5 | small, strike, strong, sub, sup, tt, var, 6 | b, u, i, center, 7 | dl, dt, dd, ol, ul, li, 8 | fieldset, form, label, legend, 9 | table, caption, tbody, tfoot, thead, tr, th, td, 10 | article, aside, canvas, details, embed, 11 | figure, figcaption, footer, header, hgroup, 12 | menu, nav, output, ruby, section, summary, 13 | time, mark, audio, video { 14 | margin: 0; 15 | padding: 0; 16 | border: 0; 17 | font-size: 100%; 18 | font: inherit; 19 | vertical-align: baseline; 20 | } 21 | /* HTML5 display-role reset for older browsers */ 22 | article, aside, details, figcaption, figure, 23 | footer, header, hgroup, menu, nav, section { 24 | display: block; 25 | } 26 | body { 27 | line-height: 1; 28 | } 29 | ol, ul { 30 | list-style: none; 31 | } 32 | blockquote, q { 33 | quotes: none; 34 | } 35 | blockquote:before, blockquote:after, 36 | q:before, q:after { 37 | content: ''; 38 | content: none; 39 | } 40 | table { 41 | border-collapse: collapse; 42 | border-spacing: 0; 43 | } 44 | -------------------------------------------------------------------------------- /src/renderer/utils/music/bd/index.js: -------------------------------------------------------------------------------- 1 | import leaderboard from './leaderboard' 2 | import api_source from '../api-source' 3 | import musicInfo from './musicInfo' 4 | import songList from './songList' 5 | import { httpFetch } from '../../request' 6 | import musicSearch from './musicSearch' 7 | 8 | const bd = { 9 | leaderboard, 10 | songList, 11 | musicSearch, 12 | getMusicUrl(songInfo, type) { 13 | return api_source('bd').getMusicUrl(songInfo, type) 14 | }, 15 | getPic(songInfo) { 16 | const requestObj = this.getMusicInfo(songInfo) 17 | requestObj.promise = requestObj.promise.then(info => info.pic_premium) 18 | return requestObj 19 | }, 20 | getLyric(songInfo) { 21 | const requestObj = this.getMusicInfo(songInfo) 22 | requestObj.promise = requestObj.promise.then(info => httpFetch(info.lrclink).promise.then(resp => resp.body)) 23 | return requestObj 24 | }, 25 | // getLyric(songInfo) { 26 | // return api_source('bd').getLyric(songInfo) 27 | // }, 28 | // getPic(songInfo) { 29 | // return api_source('bd').getPic(songInfo) 30 | // }, 31 | getMusicInfo(songInfo) { 32 | return musicInfo.getMusicInfo(songInfo.songmid) 33 | }, 34 | } 35 | 36 | export default bd 37 | -------------------------------------------------------------------------------- /src/main/events/request.js: -------------------------------------------------------------------------------- 1 | const request = require('request') 2 | 3 | const { mainOn } = require('../../common/ipc') 4 | 5 | const tasks = [] 6 | 7 | mainOn('request', (event, options) => { 8 | // console.log(args) 9 | if (!options) return 10 | let index = fetchData(options, (err, resp) => { 11 | tasks[index] = null 12 | if (err) { 13 | console.log(err) 14 | event.sender.send('response', err.message, null) 15 | return 16 | } 17 | event.sender.send('response', null, resp.body) 18 | }) 19 | event.returnValue = index 20 | }) 21 | 22 | mainOn('cancelRequest', (event, index) => { 23 | if (index == null) return 24 | let r = tasks[index] 25 | if (r == null) return 26 | r.abort() 27 | tasks[index] = null 28 | }) 29 | 30 | const fetchData = (options, callback) => pushTask(tasks, request(options.url, { 31 | method: options.method, 32 | headers: options.headers, 33 | Origin: options.origin, 34 | }, (err, resp) => { 35 | if (err) return callback(err, null) 36 | callback(null, resp) 37 | })) 38 | 39 | const pushTask = (tasks, newTask) => { 40 | for (const [index, task] of tasks.entries()) { 41 | if (task == null) { 42 | return tasks[index].push(newTask) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/renderer/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | // import { createPersistedState, createSharedMutations } from 'vuex-electron' 4 | 5 | import defaultState from './state' 6 | import mutations from './mutations' 7 | import modules from './modules' 8 | import getters from './getters' 9 | import actions from './actions' 10 | 11 | Vue.use(Vuex) 12 | 13 | const isDev = process.env.NODE_ENV === 'development' 14 | 15 | const store = new Vuex.Store({ 16 | strict: isDev, 17 | state: defaultState, 18 | modules, 19 | mutations, 20 | getters, 21 | actions, 22 | // plugins: [createPersistedState(), createSharedMutations()], 23 | }) 24 | 25 | if (module.hot) { 26 | module.hot.accept([ 27 | './state', 28 | './mutations', 29 | './actions', 30 | './getters', 31 | ], () => { 32 | const newState = require('./state').default 33 | const newMutations = require('./mutations').default 34 | const newActions = require('./actions').default 35 | const newGetters = require('./getters').default 36 | 37 | store.hotUpdate({ 38 | state: newState, 39 | mutations: newMutations, 40 | getters: newGetters, 41 | actions: newActions, 42 | }) 43 | }) 44 | } 45 | 46 | export default store 47 | -------------------------------------------------------------------------------- /src/renderer/store/actions.js: -------------------------------------------------------------------------------- 1 | // import api from 'api/connom' 2 | import { httpGet } from '../utils/request' 3 | import { author, name } from '../../../package.json' 4 | 5 | export default { 6 | getVersionInfo(state, retryNum = 0) { 7 | return new Promise((resolve, reject) => { 8 | httpGet(`https://raw.githubusercontent.com/${author.name}/${name}/master/publish/version.json`, { 9 | timeout: 20000, 10 | }, (err, resp, body) => { 11 | if (err) { 12 | return ++retryNum > 3 13 | ? this.dispatch('getVersionInfo2').then(ver => resolve(ver)).catch(err => reject(err)) 14 | : this.dispatch('getVersionInfo', retryNum).then(ver => resolve(ver)).catch(err => reject(err)) 15 | } 16 | resolve(body) 17 | }) 18 | }) 19 | }, 20 | getVersionInfo2(state, retryNum = 0) { 21 | return new Promise((resolve, reject) => { 22 | httpGet('https://cdn.stsky.cn/lx-music/desktop/version.json', { 23 | timeout: 20000, 24 | }, (err, resp, body) => { 25 | if (err) { 26 | return ++retryNum > 3 ? reject() : this.dispatch('getVersionInfo2', retryNum).then(ver => resolve(ver)).catch(err => reject(err)) 27 | } 28 | resolve(body) 29 | }) 30 | }) 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /src/renderer/utils/music/kg/lyric.js: -------------------------------------------------------------------------------- 1 | import { httpFetch } from '../../request' 2 | 3 | export default { 4 | getIntv(interval) { 5 | let intvArr = interval.split(':') 6 | let intv = 0 7 | let unit = 1 8 | while (intvArr.length) { 9 | intv += (intvArr.pop()) * unit 10 | unit *= 60 11 | } 12 | return parseInt(intv) 13 | }, 14 | getLyric(songInfo, tryNum = 0) { 15 | let requestObj = httpFetch(`http://m.kugou.com/app/i/krc.php?cmd=100&keyword=${encodeURIComponent(songInfo.name)}&hash=${songInfo.hash}&timelength=${songInfo._interval || this.getIntv(songInfo.interval)}&d=0.38664927426725626`, { 16 | headers: { 17 | 'KG-RC': 1, 18 | 'KG-THash': 'expand_search_manager.cpp:852736169:451', 19 | 'User-Agent': 'KuGou2012-9020-ExpandSearchManager', 20 | }, 21 | }) 22 | requestObj.promise = requestObj.promise.then(({ body, statusCode }) => { 23 | if (statusCode !== 200) { 24 | if (tryNum > 5) return Promise.reject('歌词获取失败') 25 | let tryRequestObj = this.getLyric(songInfo, ++tryNum) 26 | requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj) 27 | return tryRequestObj.promise 28 | } 29 | return body 30 | }) 31 | return requestObj 32 | }, 33 | } 34 | -------------------------------------------------------------------------------- /src/main/utils/flacMeta.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const flac = require('flac-metadata') 3 | 4 | module.exports = (filenPath, meta) => { 5 | const reader = fs.createReadStream(filenPath) 6 | const tempPath = filenPath + '.lxmtemp' 7 | const writer = fs.createWriteStream(tempPath) 8 | const processor = new flac.Processor() 9 | if (meta.APIC) delete meta.APIC 10 | 11 | const comments = [] 12 | for (const key in meta) { 13 | comments.push(`${key.toUpperCase()}=${meta[key]}`) 14 | } 15 | const vendor = 'lx-music-desktop' 16 | 17 | processor.on('preprocess', function(mdb) { 18 | // Remove existing VORBIS_COMMENT block, if any. 19 | if (mdb.type === flac.Processor.MDB_TYPE_VORBIS_COMMENT) { 20 | mdb.remove() 21 | } 22 | // Inject new VORBIS_COMMENT block. 23 | if (mdb.removed || mdb.isLast) { 24 | let mdbVorbis = flac.data.MetaDataBlockVorbisComment.create(mdb.isLast, vendor, comments) 25 | this.push(mdbVorbis.publish()) 26 | } 27 | }) 28 | 29 | reader.pipe(processor).pipe(writer).on('finish', () => { 30 | fs.unlink(filenPath, err => { 31 | if (err) return console.log(err.message) 32 | fs.rename(tempPath, filenPath, err => { 33 | if (err) console.log(err.message) 34 | }) 35 | }) 36 | }) 37 | } 38 | 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | node_modules.bak*/ 38 | jspm_packages/ 39 | 40 | # TypeScript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | # next.js build output 62 | .next 63 | 64 | 65 | build 66 | 67 | dist 68 | 69 | publish/assets 70 | 71 | publish/utils/githubToken.js 72 | 73 | src/**/*-internal.js 74 | -------------------------------------------------------------------------------- /src/renderer/utils/music/kg/pic.js: -------------------------------------------------------------------------------- 1 | import { httpFetch } from '../../request' 2 | 3 | export default { 4 | getPic(songInfo) { 5 | const requestObj = httpFetch( 6 | 'http://media.store.kugou.com/v1/get_res_privilege', 7 | { 8 | method: 'POST', 9 | headers: { 10 | 'KG-RC': 1, 11 | 'KG-THash': 'expand_search_manager.cpp:852736169:451', 12 | 'User-Agent': 'KuGou2012-9020-ExpandSearchManager', 13 | }, 14 | body: { 15 | appid: 1001, 16 | area_code: '1', 17 | behavior: 'play', 18 | clientver: '9020', 19 | need_hash_offset: 1, 20 | relate: 1, 21 | resource: [ 22 | { 23 | album_audio_id: songInfo.songmid, 24 | album_id: songInfo.albumId, 25 | hash: songInfo.hash, 26 | id: 0, 27 | name: `${songInfo.singer} - ${songInfo.name}.mp3`, 28 | type: 'audio', 29 | }, 30 | ], 31 | token: '', 32 | userid: 2626431536, 33 | vip: 1, 34 | }, 35 | }, 36 | ) 37 | requestObj.promise = requestObj.promise.then(({ body }) => { 38 | if (body.error_code !== 0) return Promise.reject('图片获取失败') 39 | let info = body.data[0].info 40 | return info.imgsize ? info.image.replace('{size}', info.imgsize[0]) : info.image 41 | }) 42 | return requestObj 43 | }, 44 | } 45 | -------------------------------------------------------------------------------- /src/renderer/utils/music/mg/lyric.js: -------------------------------------------------------------------------------- 1 | import { httpFetch } from '../../request' 2 | 3 | export default { 4 | getLyric(songInfo, tryNum = 0) { 5 | console.log(songInfo.copyrightId) 6 | if (songInfo.lrcUrl) { 7 | let requestObj = httpFetch(songInfo.lrcUrl) 8 | requestObj.promise = requestObj.promise.then(({ body, statusCode }) => { 9 | if (statusCode !== 200) { 10 | if (tryNum > 5) return Promise.reject('歌词获取失败') 11 | let tryRequestObj = this.getLyric(songInfo, ++tryNum) 12 | requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj) 13 | return tryRequestObj.promise 14 | } 15 | return body 16 | }) 17 | return requestObj 18 | } else { 19 | let requestObj = httpFetch(`http://music.migu.cn/v3/api/music/audioPlayer/getLyric?copyrightId=${songInfo.copyrightId}`, { 20 | headers: { 21 | Referer: 'http://music.migu.cn/v3/music/player/audio?from=migu', 22 | }, 23 | }) 24 | requestObj.promise = requestObj.promise.then(({ body }) => { 25 | if (body.returnCode !== '000000') { 26 | if (tryNum > 5) return Promise.reject('歌词获取失败') 27 | let tryRequestObj = this.getLyric(songInfo, ++tryNum) 28 | requestObj.cancelHttp = tryRequestObj.cancelHttp.bind(tryRequestObj) 29 | return tryRequestObj.promise 30 | } 31 | return body.lyric 32 | }) 33 | return requestObj 34 | } 35 | }, 36 | } 37 | -------------------------------------------------------------------------------- /src/renderer/utils/music/kw/tempSearch.js: -------------------------------------------------------------------------------- 1 | import { httpFetch } from '../../request' 2 | import { decodeName } from '../../index' 3 | import { getToken, matchToken } from './util' 4 | 5 | 6 | export default { 7 | regExps: { 8 | relWord: /RELWORD=(.+)/, 9 | }, 10 | requestObj: null, 11 | tempSearch(str, token) { 12 | this.cancelTempSearch() 13 | this.requestObj = httpFetch(`http://www.kuwo.cn/api/www/search/searchKey?key=${encodeURIComponent(str)}`, { 14 | headers: { 15 | Referer: 'http://www.kuwo.cn/', 16 | csrf: token, 17 | cookie: 'kw_token=' + token, 18 | }, 19 | }) 20 | return this.requestObj.promise.then(({ statusCode, body, headers }) => { 21 | if (statusCode != 200) return Promise.reject(new Error('请求失败')) 22 | window.kw_token.token = matchToken(headers) 23 | if (body.code !== 200) return Promise.reject(new Error('请求失败')) 24 | return body 25 | }) 26 | }, 27 | handleResult(rawData) { 28 | return rawData.map(info => { 29 | let matchResult = info.match(this.regExps.relWord) 30 | return matchResult ? decodeName(matchResult[1]) : '' 31 | }) 32 | }, 33 | cancelTempSearch() { 34 | if (this.requestObj && this.requestObj.cancelHttp) this.requestObj.cancelHttp() 35 | }, 36 | async search(str) { 37 | let token = window.kw_token.token 38 | if (!token) token = await getToken() 39 | return this.tempSearch(str, token).then(result => this.handleResult(result.data)) 40 | }, 41 | } 42 | -------------------------------------------------------------------------------- /src/renderer/components/material/Btn.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 19 | 20 | 21 | 67 | -------------------------------------------------------------------------------- /src/main/utils/mp3Meta.js: -------------------------------------------------------------------------------- 1 | const NodeID3 = require('node-id3') 2 | const path = require('path') 3 | const fs = require('fs') 4 | const request = require('request') 5 | const extReg = /^(\.(?:jpe?g|png)).*$/ 6 | 7 | module.exports = (filePath, meta) => { 8 | if (!meta.APIC) return NodeID3.write(meta, filePath) 9 | if (!/^http/.test(meta.APIC)) { 10 | delete meta.APIC 11 | return NodeID3.write(meta, filePath) 12 | } 13 | let picPath = filePath.replace(/\.mp3$/, '') + path.extname(meta.APIC).replace(extReg, '$1') 14 | request(meta.APIC) 15 | .on('response', respones => { 16 | if (respones.statusCode !== 200 && respones.statusCode != 206) { 17 | delete meta.APIC 18 | NodeID3.write(meta, filePath) 19 | return 20 | } 21 | respones 22 | .pipe(fs.createWriteStream(picPath)) 23 | .on('finish', () => { 24 | if (respones.complete) { 25 | meta.APIC = picPath 26 | NodeID3.write(meta, filePath) 27 | } else { 28 | delete meta.APIC 29 | } 30 | fs.unlink(picPath, err => { 31 | if (err) console.log(err.message) 32 | }) 33 | }).on('error', err => { 34 | if (err) console.log(err.message) 35 | delete meta.APIC 36 | NodeID3.write(meta, filePath) 37 | }) 38 | }) 39 | .on('error', err => { 40 | if (err) console.log(err.message) 41 | delete meta.APIC 42 | NodeID3.write(meta, filePath) 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /src/renderer/utils/music/kg/api-test.js: -------------------------------------------------------------------------------- 1 | import { httpFetch } from '../../request' 2 | import { requestMsg } from '../../message' 3 | import { headers, timeout } from '../options' 4 | 5 | const api_test = { 6 | getMusicUrl(songInfo, type) { 7 | const requestObj = httpFetch(`http://ts.tempmusic.tk/url/kg/${songInfo._types[type].hash}/${type}`, { 8 | method: 'get', 9 | timeout, 10 | headers, 11 | family: 4, 12 | }) 13 | requestObj.promise = requestObj.promise.then(({ body }) => { 14 | return body.code === 0 ? Promise.resolve({ type, url: body.data }) : Promise.reject(new Error(requestMsg.fail)) 15 | }) 16 | return requestObj 17 | }, 18 | getPic(songInfo) { 19 | const requestObj = httpFetch(`http://ts.tempmusic.tk/pic/kg/${songInfo.hash}`, { 20 | method: 'get', 21 | timeout, 22 | headers, 23 | family: 4, 24 | }) 25 | requestObj.promise = requestObj.promise.then(({ body }) => { 26 | return body.code === 0 ? Promise.resolve(body.data) : Promise.reject(new Error(requestMsg.fail)) 27 | }) 28 | return requestObj 29 | }, 30 | getLyric(songInfo) { 31 | const requestObj = httpFetch(`http://ts.tempmusic.tk/lrc/kg/${songInfo.hash}`, { 32 | method: 'get', 33 | timeout, 34 | headers, 35 | family: 4, 36 | }) 37 | requestObj.promise = requestObj.promise.then(({ body }) => { 38 | return body.code === 0 ? Promise.resolve(body.data) : Promise.reject(new Error(requestMsg.fail)) 39 | }) 40 | return requestObj 41 | }, 42 | } 43 | 44 | export default api_test 45 | -------------------------------------------------------------------------------- /src/renderer/utils/music/wy/api-test.js: -------------------------------------------------------------------------------- 1 | import { httpFetch } from '../../request' 2 | import { requestMsg } from '../../message' 3 | import { headers, timeout } from '../options' 4 | 5 | const api_test = { 6 | getMusicUrl(songInfo, type) { 7 | const requestObj = httpFetch(`http://ts.tempmusic.tk/url/wy/${songInfo.songmid}/${type}`, { 8 | method: 'get', 9 | timeout, 10 | headers, 11 | family: 4, 12 | }) 13 | requestObj.promise = requestObj.promise.then(({ body }) => { 14 | return body.code === 0 ? Promise.resolve({ type, url: body.data }) : Promise.reject(new Error(requestMsg.fail)) 15 | }) 16 | return requestObj 17 | }, 18 | /* getPic(songInfo) { 19 | const requestObj = httpFetch(`http://localhost:3100/pic/wy/${songInfo.songmid}`, { 20 | method: 'get', 21 | timeout, 22 | headers, 23 | family: 4, 24 | }) 25 | requestObj.promise = requestObj.promise.then(({ body }) => { 26 | return body.code === 0 ? Promise.resolve(body.data) : Promise.reject(new Error(requestMsg.fail)) 27 | }) 28 | return requestObj 29 | }, 30 | getLyric(songInfo) { 31 | const requestObj = httpFetch(`http://localhost:3100/lrc/wy/${songInfo.songmid}`, { 32 | method: 'get', 33 | timeout, 34 | headers, 35 | family: 4, 36 | }) 37 | requestObj.promise = requestObj.promise.then(({ body }) => { 38 | return body.code === 0 ? Promise.resolve(body.data) : Promise.reject(new Error(requestMsg.fail)) 39 | }) 40 | return requestObj 41 | }, */ 42 | } 43 | 44 | export default api_test 45 | -------------------------------------------------------------------------------- /src/renderer/utils/music/api-source.js: -------------------------------------------------------------------------------- 1 | import kw_api_temp from './kw/api-temp' 2 | import kw_api_test from './kw/api-test' 3 | import tx_api_test from './tx/api-test' 4 | import kg_api_test from './kg/api-test' 5 | import wy_api_test from './wy/api-test' 6 | import bd_api_test from './bd/api-test' 7 | import mg_api_test from './mg/api-test' 8 | // import kw_api_internal from './kw/api-internal' 9 | // import tx_api_internal from './tx/api-internal' 10 | // import kg_api_internal from './kg/api-internal' 11 | // import wy_api_internal from './wy/api-internal' 12 | // import bd_api_internal from './bd/api-internal' 13 | 14 | const apis = { 15 | kw_api_test, 16 | tx_api_test, 17 | kg_api_test, 18 | wy_api_test, 19 | bd_api_test, 20 | mg_api_test, 21 | // kw_api_internal, 22 | // tx_api_internal, 23 | // kg_api_internal, 24 | // wy_api_internal, 25 | // bd_api_internal, 26 | kw_api_temp, 27 | } 28 | 29 | 30 | const getAPI = source => { 31 | switch (window.globalObj.apiSource) { 32 | // case 'messoer': 33 | // return apis[`${source}_api_messoer`] 34 | case 'test': 35 | return apis[`${source}_api_test`] 36 | case 'temp': 37 | return apis[`${source}_api_temp`] 38 | } 39 | } 40 | 41 | export default source => { 42 | switch (source) { 43 | case 'tx': 44 | return getAPI('tx') 45 | case 'kg': 46 | return getAPI('kg') 47 | case 'wy': 48 | return getAPI('wy') 49 | case 'bd': 50 | return getAPI('bd') 51 | case 'mg': 52 | return getAPI('mg') 53 | default: 54 | return getAPI('kw') 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/renderer/utils/music/kg/api-internal.js: -------------------------------------------------------------------------------- 1 | import { httpFatch } from '../../request' 2 | import { requestMsg } from '../../message' 3 | import { headers, timeout } from '../options' 4 | 5 | const api_messoer = { 6 | getMusicUrl(songInfo, type) { 7 | const requestObj = httpFatch(`https://v1.itooi.cn/kugou/url?id=${songInfo._types[type].hash}&quality=${type.replace(/k$/, '')}&isRedirect=0`, { 8 | method: 'get', 9 | timeout, 10 | headers, 11 | }) 12 | requestObj.promise = requestObj.promise.then(({ body }) => { 13 | return body.code === 200 ? Promise.resolve({ type, url: body.data }) : Promise.reject(new Error(requestMsg.fail)) 14 | }) 15 | return requestObj 16 | }, 17 | getPic(songInfo) { 18 | const requestObj = httpFatch(`https://v1.itooi.cn/kugou/pic?id=${songInfo.hash}&isRedirect=0`, { 19 | method: 'get', 20 | timeout, 21 | headers, 22 | }) 23 | requestObj.promise = requestObj.promise.then(({ body }) => { 24 | return body.code === 200 ? Promise.resolve(body.data) : Promise.reject(new Error(requestMsg.fail)) 25 | }) 26 | return requestObj 27 | }, 28 | getLyric(songInfo) { 29 | const requestObj = httpFatch(`https://v1.itooi.cn/kugou/lrc?id=${songInfo.hash}&isRedirect=0`, { 30 | method: 'get', 31 | timeout, 32 | headers, 33 | }) 34 | requestObj.promise = requestObj.promise.then(({ body }) => { 35 | return body ? Promise.resolve(body) : Promise.reject(new Error(requestMsg.fail)) 36 | }) 37 | return requestObj 38 | }, 39 | } 40 | 41 | export default api_messoer 42 | -------------------------------------------------------------------------------- /src/renderer/store/modules/leaderboard.js: -------------------------------------------------------------------------------- 1 | import music from '../../utils/music' 2 | const sourceList = {} 3 | const sources = [] 4 | for (const source of music.sources) { 5 | const leaderboard = music[source.id].leaderboard 6 | if (!leaderboard) continue 7 | sourceList[source.id] = leaderboard.list 8 | sources.push(source) 9 | } 10 | 11 | // state 12 | const state = { 13 | list: [], 14 | total: 0, 15 | page: 1, 16 | limit: 30, 17 | key: null, 18 | } 19 | 20 | // getters 21 | const getters = { 22 | sourceInfo: () => ({ sources, sourceList }), 23 | list(state) { 24 | return state.list 25 | }, 26 | info(state) { 27 | return { 28 | total: state.total, 29 | limit: state.limit, 30 | page: state.page, 31 | } 32 | }, 33 | } 34 | 35 | // actions 36 | const actions = { 37 | getList({ state, rootState, commit }, page) { 38 | let source = rootState.setting.leaderboard.source 39 | let tabId = rootState.setting.leaderboard.tabId 40 | let key = `${source}${tabId}${page}}` 41 | if (state.list.length && state.key == key) return true 42 | commit('clearList') 43 | return music[source].leaderboard.getList(tabId, page).then(result => commit('setList', { result, key })) 44 | }, 45 | } 46 | 47 | // mitations 48 | const mutations = { 49 | setList(state, { result, key }) { 50 | state.list = result.list 51 | state.total = result.total 52 | state.limit = result.limit 53 | state.page = result.page 54 | state.key = key 55 | }, 56 | clearList(state) { 57 | state.list = [] 58 | state.total = 0 59 | }, 60 | } 61 | 62 | export default { 63 | namespaced: true, 64 | state, 65 | getters, 66 | actions, 67 | mutations, 68 | } 69 | -------------------------------------------------------------------------------- /publish/utils/githubRelease.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const ghRelease = require('gh-release') 3 | const token = require('./githubToken') 4 | const pkg = require('../../package.json') 5 | const { jp } = require('./index') 6 | 7 | const changeLog = fs.readFileSync(jp('../changeLog.md'), 'utf-8') 8 | 9 | const assetsDir = '../assets' 10 | 11 | const getBuildFiles = () => { 12 | const files = [] 13 | const pathRegExp = [ 14 | /latest\.yml$/, 15 | /\.exe$/, 16 | /\.blockmap$/, 17 | ] 18 | const names = fs.readdirSync(jp(assetsDir), 'utf8') 19 | names.forEach(name => { 20 | pathRegExp.forEach(regexp => { 21 | if (regexp.test(name)) files.push(jp(assetsDir, name)) 22 | }) 23 | }) 24 | return files 25 | } 26 | 27 | // all options have defaults and can be omitted 28 | const options = { 29 | tag_name: `v${pkg.version}`, 30 | target_commitish: 'master', 31 | name: `v${pkg.version}`, 32 | body: changeLog, 33 | draft: false, 34 | prerelease: false, 35 | repo: pkg.name, 36 | owner: pkg.author, 37 | endpoint: 'https://api.github.com', // for GitHub enterprise, use http(s)://hostname/api/v3 38 | auth: { 39 | token, 40 | }, 41 | assets: getBuildFiles(), 42 | } 43 | 44 | 45 | module.exports = ({ isDraft = false, isPrerelease = false, target_commitish = 'master' }) => new Promise((resolve, reject) => { 46 | options.target_commitish = target_commitish 47 | options.draft = isDraft 48 | options.prerelease = isPrerelease 49 | 50 | ghRelease(options, function(err, result) { 51 | if (err) return reject(err) 52 | resolve(result) 53 | console.log(result) // create release response: https://developer.github.com/v3/repos/releases/#response-4 54 | }) 55 | }) 56 | 57 | -------------------------------------------------------------------------------- /src/renderer/route/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | import paths from './paths' 5 | 6 | 7 | function route(path, view, name, meta, props) { 8 | return { 9 | name: name || view, 10 | path, 11 | meta, 12 | props, 13 | component: (resovle) => import(`../views/${view}.vue`).then(resovle), 14 | } 15 | } 16 | 17 | Vue.use(Router) 18 | 19 | const router = new Router({ 20 | mode: 'hash', 21 | routes: paths.map(path => route(path.path, path.view, path.name, path.meta, path.props)).concat([ 22 | { path: '*', redirect: '/search' }, 23 | ]), 24 | linkActiveClass: 'active-link', 25 | linkExactActiveClass: 'exact-active-link', 26 | scrollBehavior(to, from, savedPosition) { 27 | return new Promise((resolve, reject) => { 28 | setTimeout(() => { 29 | if (savedPosition) { 30 | resolve(savedPosition) 31 | } else { 32 | const position = {} 33 | // new navigation. 34 | // scroll to anchor by returning the selector 35 | if (to.hash) { 36 | position.selector = to.hash 37 | } 38 | // check if any matched route config has meta that requires scrolling to top 39 | if (to.matched.some(m => m.meta.scrollToTop)) { 40 | // cords will be used if no selector is provided, 41 | // or if the selector didn't match any element. 42 | position.x = 0 43 | position.y = 0 44 | } 45 | // if the returned position is falsy or an empty object, 46 | // will retain current scroll position. 47 | resolve(position) 48 | } 49 | }, 500) 50 | }) 51 | }, 52 | }) 53 | 54 | 55 | export default router 56 | -------------------------------------------------------------------------------- /src/renderer/utils/music/wy/utils/crypto.js: -------------------------------------------------------------------------------- 1 | // https://github.com/Binaryify/NeteaseCloudMusicApi/blob/master/util/crypto.js 2 | import { createCipheriv, publicEncrypt, constants, randomBytes } from 'crypto' 3 | const iv = Buffer.from('0102030405060708') 4 | const presetKey = Buffer.from('0CoJUm6Qyw8W8jud') 5 | const linuxapiKey = Buffer.from('rFgB&h#%2?^eDg:Q') 6 | const base62 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' 7 | const publicKey = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBGLbCiK45wIDAQAB\n-----END PUBLIC KEY-----' 8 | 9 | const aesEncrypt = (buffer, mode, key, iv) => { 10 | const cipher = createCipheriv('aes-128-' + mode, key, iv) 11 | return Buffer.concat([cipher.update(buffer), cipher.final()]) 12 | } 13 | 14 | const rsaEncrypt = (buffer, key) => { 15 | buffer = Buffer.concat([Buffer.alloc(128 - buffer.length), buffer]) 16 | return publicEncrypt({ key: key, padding: constants.RSA_NO_PADDING }, buffer) 17 | } 18 | 19 | export const weapi = object => { 20 | const text = JSON.stringify(object) 21 | const secretKey = randomBytes(16).map(n => (base62.charAt(n % 62).charCodeAt())) 22 | return { 23 | params: aesEncrypt(Buffer.from(aesEncrypt(Buffer.from(text), 'cbc', presetKey, iv).toString('base64')), 'cbc', secretKey, iv).toString('base64'), 24 | encSecKey: rsaEncrypt(secretKey.reverse(), publicKey).toString('hex'), 25 | } 26 | } 27 | 28 | export const linuxapi = object => { 29 | const text = JSON.stringify(object) 30 | return { 31 | eparams: aesEncrypt(Buffer.from(text), 'ecb', linuxapiKey, '').toString('hex').toUpperCase(), 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/renderer/components/material/InputRange.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 36 | 37 | 38 | 81 | -------------------------------------------------------------------------------- /src/renderer/components/material/DownloadMultipleModal.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 43 | 44 | 45 | 83 | -------------------------------------------------------------------------------- /publish/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const chalk = require('chalk') 4 | const clearAssets = require('./utils/clearAssets') 5 | // const packAssets = require('./utils/packAssets') 6 | // const compileAssets = require('./utils/compileAssets') 7 | const updateVersionFile = require('./utils/updateChangeLog') 8 | // const copyFile = require('./utils/copyFile') 9 | // const githubRelease = require('./utils/githubRelease') 10 | // const { parseArgv } = require('./utils') 11 | 12 | const run = async() => { 13 | // const params = parseArgv(process.argv.slice(2)) 14 | // const bak = await updateVersionFile(params.ver) 15 | const bak = await updateVersionFile(process.argv.slice(2)[0]) 16 | 17 | try { 18 | console.log(chalk.blue('Clearing assets...')) 19 | await clearAssets() 20 | console.log(chalk.green('Assets clear completed...')) 21 | 22 | // console.log(chalk.blue('Compileing assets...')) 23 | // await compileAssets() 24 | // console.log(chalk.green('Asset compiled successfully.')) 25 | 26 | // console.log(chalk.blue('Building assets...')) 27 | // await packAssets() 28 | // console.log(chalk.green('Asset build successfully.')) 29 | 30 | // console.log(chalk.blue('Copy files...')) 31 | // await copyFile() 32 | // console.log(chalk.green('Complete copy of all files.')) 33 | 34 | // console.log(chalk.blue('Create release...')) 35 | // await githubRelease(params) 36 | // console.log(chalk.green('Release created.')) 37 | 38 | console.log(chalk.green('日志更新完成~')) 39 | } catch (error) { 40 | console.log(error) 41 | console.log(chalk.red('程序发布失败')) 42 | console.log(chalk.blue('正在还原版本信息')) 43 | fs.writeFileSync(path.join(__dirname, './version.json'), bak.version_bak + '\n', 'utf-8') 44 | fs.writeFileSync(path.join(__dirname, '../package.json'), bak.pkg_bak + '\n', 'utf-8') 45 | console.log(chalk.blue('版本信息还原完成')) 46 | } 47 | } 48 | 49 | 50 | run() 51 | -------------------------------------------------------------------------------- /src/renderer/utils/music/bd/api-internal.js: -------------------------------------------------------------------------------- 1 | import { httpFatch } from '../../request' 2 | import { requestMsg } from '../../message' 3 | 4 | const api_internal = { 5 | successCode: 2200, 6 | // getMusicUrl(songInfo, type) { 7 | // const requestObj = httpFatch(`http://play.taihe.com/data/music/songlink`, { 8 | // method: 'post', 9 | // headers: { 10 | // Origin: 'http://play.taihe.com', 11 | // }, 12 | // formData: { 13 | // songIds: songInfo.songmid, 14 | // }, 15 | // }) 16 | // requestObj.promise = requestObj.promise.then(({ body }) => { 17 | // console.log(body) 18 | // return Promise.reject() 19 | // // if (body.error_code !== this.successCode) return this.getMusicUrl(songInfo, type) 20 | // // return body.code === 200 ? Promise.resolve({ type, url: body.result.bitrate.file_link }) : Promise.reject(new Error(requestMsg.fail)) 21 | // }) 22 | // return requestObj 23 | // }, 24 | getMusicInfo(songInfo, tryNum = 0) { 25 | const requestObj = httpFatch(`http://tingapi.ting.baidu.com/v1/restserver/ting?from=webapp_music&method=baidu.ting.song.baseInfos&song_id=${songInfo.songmid}`) 26 | requestObj.promise = requestObj.promise.then(({ body }) => { 27 | if (body.error_code !== this.successCode) return tryNum > 5 ? Promise.reject(new Error(requestMsg.fail)) : this.getMusicInfo(songInfo, ++tryNum) 28 | return body ? Promise.resolve(body.result.items[0]) : Promise.reject(new Error(requestMsg.fail)) 29 | }) 30 | return requestObj 31 | }, 32 | getPic(songInfo) { 33 | const requestObj = this.getMusicInfo(songInfo) 34 | requestObj.promise = requestObj.promise.then(info => info.pic_premium) 35 | return requestObj 36 | }, 37 | getLyric(songInfo) { 38 | const requestObj = this.getMusicInfo(songInfo) 39 | requestObj.promise.then(info => httpFatch(info.lrclink).promise) 40 | return requestObj 41 | }, 42 | } 43 | 44 | export default api_internal 45 | -------------------------------------------------------------------------------- /src/renderer/store/mutations.js: -------------------------------------------------------------------------------- 1 | export default { 2 | setTheme(state, val) { 3 | state.setting.themeId = val 4 | }, 5 | setSearchSource(state, { searchSource, tempSearchSource }) { 6 | if (searchSource != null) state.setting.search.searchSource = searchSource 7 | if (tempSearchSource != null) state.setting.search.tempSearchSource = tempSearchSource 8 | }, 9 | setSetting(state, val) { 10 | state.setting = val 11 | }, 12 | setSettingVersion(state, val) { 13 | state.settingVersion = val 14 | }, 15 | setLeaderboard(state, { tabId, source }) { 16 | if (tabId != null) state.setting.leaderboard.tabId = tabId 17 | if (source != null) state.setting.leaderboard.source = source 18 | }, 19 | setSongList(state, { sortId, tagInfo, source }) { 20 | if (tagInfo != null) state.setting.songList.tagInfo = tagInfo 21 | if (sortId != null) state.setting.songList.sortId = sortId 22 | if (source != null) state.setting.songList.source = source 23 | }, 24 | setListScroll(state, { id, location }) { 25 | state.setting.list.scroll.locations[id] = location 26 | }, 27 | setNewVersion(state, val) { 28 | state.version.newVersion = val 29 | }, 30 | setDownloadProgress(state, info) { 31 | state.version.downloadProgress = info 32 | }, 33 | setVersionModalVisible(state, { isShow, isError, isDownloaded, isTimeOut, isDownloading, isUnknow, isLatestVer }) { 34 | if (isShow !== undefined) state.version.showModal = isShow 35 | if (isError !== undefined) state.version.isError = isError 36 | if (isTimeOut !== undefined) state.version.isTimeOut = isTimeOut 37 | if (isDownloading !== undefined) state.version.isDownloading = isDownloading 38 | if (isDownloaded !== undefined) state.version.isDownloaded = isDownloaded 39 | if (isUnknow !== undefined) state.version.isUnknow = isUnknow 40 | if (isLatestVer !== undefined) state.version.isLatestVer = isLatestVer 41 | }, 42 | setVolume(state, val) { 43 | state.setting.player.volume = val 44 | }, 45 | } 46 | -------------------------------------------------------------------------------- /publish/utils/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | exports.jp = (...p) => p.length ? path.join(__dirname, ...p) : __dirname 5 | 6 | exports.copyFile = (source, target) => new Promise((resolve, reject) => { 7 | const rd = fs.createReadStream(source) 8 | rd.on('error', err => reject(err)) 9 | const wr = fs.createWriteStream(target) 10 | wr.on('error', err => reject(err)) 11 | wr.on('close', () => resolve()) 12 | rd.pipe(wr) 13 | }) 14 | 15 | /** 16 | * 时间格式化 17 | * @param {Date} d 格式化的时间 18 | * @param {boolean} b 是否精确到秒 19 | */ 20 | exports.formatTime = (d, b) => { 21 | const _date = d == null ? new Date() : typeof d == 'string' ? new Date(d) : d 22 | const year = _date.getFullYear() 23 | const month = fm(_date.getMonth() + 1) 24 | const day = fm(_date.getDate()) 25 | if (!b) return year + '-' + month + '-' + day 26 | return year + '-' + month + '-' + day + ' ' + fm(_date.getHours()) + ':' + fm(_date.getMinutes()) + ':' + fm(_date.getSeconds()) 27 | } 28 | 29 | function fm(value) { 30 | if (value < 10) return '0' + value 31 | return value 32 | } 33 | 34 | exports.sizeFormate = size => { 35 | // https://gist.github.com/thomseddon/3511330 36 | if (!size) return '0 b' 37 | let units = ['b', 'kB', 'MB', 'GB', 'TB'] 38 | let number = Math.floor(Math.log(size) / Math.log(1024)) 39 | return `${(size / Math.pow(1024, Math.floor(number))).toFixed(2)} ${units[number]}` 40 | } 41 | 42 | exports.parseArgv = argv => { 43 | const params = {} 44 | argv.forEach(item => { 45 | const argv = item.split('=') 46 | switch (argv[0]) { 47 | case 'ver': 48 | params.ver = argv[1] 49 | break 50 | case 'draft': 51 | params.isDraft = argv[1] === 'true' || argv[1] === undefined 52 | break 53 | case 'prerelease': 54 | params.isPrerelease = argv[1] === 'true' || argv[1] === undefined 55 | break 56 | case 'target_commitish': 57 | params.target_commitish = argv[1] 58 | break 59 | } 60 | }) 61 | return params 62 | } 63 | -------------------------------------------------------------------------------- /src/renderer/components/material/Input.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 44 | 45 | 46 | 91 | -------------------------------------------------------------------------------- /publish/utils/updateChangeLog.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const { jp, formatTime } = require('./index') 3 | const pkgDir = '../../package.json' 4 | const pkg = require(pkgDir) 5 | const version = require('../version.json') 6 | const chalk = require('chalk') 7 | const pkg_bak = JSON.stringify(pkg, null, 2) 8 | const version_bak = JSON.stringify(version, null, 2) 9 | const parseChangelog = require('changelog-parser') 10 | const changelogPath = jp('../../CHANGELOG.md') 11 | 12 | const md_renderer = markdownStr => new (require('markdown-it'))({ 13 | html: true, 14 | linkify: true, 15 | typographer: true, 16 | breaks: true, 17 | }).render(markdownStr) 18 | 19 | const getPrevVer = () => parseChangelog(changelogPath).then(res => { 20 | if (!res.versions.length) throw new Error('CHANGELOG 无法解析到版本号') 21 | return res.versions[0].version 22 | }) 23 | 24 | const updateChangeLog = async(newVerNum, newChangeLog) => { 25 | let changeLog = fs.readFileSync(changelogPath, 'utf-8') 26 | const prevVer = await getPrevVer() 27 | const log = `## [${newVerNum}](${pkg.repository.url.replace(/^git\+(http.+)\.git$/, '$1')}/compare/v${prevVer}...v${newVerNum}) - ${formatTime()}\n\n${newChangeLog}` 28 | fs.writeFileSync(changelogPath, changeLog.replace(new RegExp(`(## [?0.1.1]?)`), log + '\n$1'), 'utf-8') 29 | } 30 | 31 | const renderChangeLog = md => md_renderer(md) 32 | 33 | 34 | module.exports = async newVerNum => { 35 | if (!newVerNum) { 36 | let verArr = pkg.version.split('.') 37 | verArr[verArr.length - 1] = parseInt(verArr[verArr.length - 1]) + 1 38 | newVerNum = verArr.join('.') 39 | } 40 | const newMDChangeLog = fs.readFileSync(jp('../changeLog.md'), 'utf-8') 41 | const newChangeLog = renderChangeLog(newMDChangeLog) 42 | version.history.unshift({ 43 | version: version.version, 44 | desc: version.desc, 45 | }) 46 | version.version = newVerNum 47 | version.desc = newChangeLog 48 | pkg.version = newVerNum 49 | 50 | console.log(chalk.blue('new version: ') + chalk.green(newVerNum)) 51 | 52 | fs.writeFileSync(jp('../version.json'), JSON.stringify(version, null, 2) + '\n', 'utf-8') 53 | 54 | fs.writeFileSync(jp(pkgDir), JSON.stringify(pkg, null, 2) + '\n', 'utf-8') 55 | 56 | await updateChangeLog(newVerNum, newMDChangeLog) 57 | 58 | return { 59 | pkg_bak, 60 | version_bak, 61 | changeLog: newChangeLog, 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /src/renderer/components/material/listAddMultipleModal.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 58 | 59 | 60 | 98 | -------------------------------------------------------------------------------- /src/renderer/components/material/DownloadModal.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 65 | 66 | 67 | 105 | -------------------------------------------------------------------------------- /src/renderer/components/material/listAddModal.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 57 | 58 | 59 | 104 | -------------------------------------------------------------------------------- /src/renderer/utils/download/index.js: -------------------------------------------------------------------------------- 1 | import Downloader from './Downloader' 2 | // import { pauseResumeTimer } from './util' 3 | import { sizeFormate, getProxyInfo } from '../index' 4 | import { debugDownload } from '../env' 5 | 6 | // these are the default options 7 | // const options = { 8 | // method: 'GET', // Request Method Verb 9 | // // Custom HTTP Header ex: Authorization, User-Agent 10 | // headers: {}, 11 | // fileName: '', // Custom filename when saved 12 | // override: false, // if true it will override the file, otherwise will append '(number)' to the end of file 13 | // forceResume: false, // If the server does not return the "accept-ranges" header, can be force if it does support it 14 | // // httpRequestOptions: {}, // Override the http request options 15 | // // httpsRequestOptions: {}, // Override the https request options, ex: to add SSL Certs 16 | // } 17 | 18 | export default ({ 19 | url, 20 | path, 21 | fileName, 22 | method = 'get', 23 | headers, 24 | forceResume, 25 | // resumeTime = 5000, 26 | onCompleted = () => {}, 27 | onError = () => {}, 28 | onFail = () => {}, 29 | onStart = () => {}, 30 | onStop = () => {}, 31 | onProgress = () => {}, 32 | } = {}) => { 33 | const dl = new Downloader(url, path, fileName, { 34 | requestOptions: { 35 | method, 36 | headers, 37 | proxy: getProxyInfo(), 38 | }, 39 | 40 | forceResume, 41 | }) 42 | 43 | dl.on('completed', () => { 44 | onCompleted() 45 | debugDownload && console.log('Download Completed') 46 | }).on('error', err => { 47 | if (err.message === 'socket hang up') return 48 | onError(err) 49 | debugDownload && console.error('Something happend', err) 50 | }).on('start', () => { 51 | onStart() 52 | // pauseResumeTimer(dl, resumeTime) 53 | }).on('progress', stats => { 54 | const progress = stats.progress.toFixed(2) 55 | const speed = sizeFormate(stats.speed) 56 | onProgress({ 57 | progress, 58 | speed, 59 | downloaded: stats.downloaded, 60 | total: stats.total, 61 | }) 62 | if (debugDownload) { 63 | const downloaded = sizeFormate(stats.downloaded) 64 | const total = sizeFormate(stats.total) 65 | console.log(`${speed}/s - ${progress}% [${downloaded}/${total}]`) 66 | } 67 | }).on('stop', () => { 68 | onStop() 69 | debugDownload && console.log('paused') 70 | }).on('fail', resp => { 71 | onFail(resp) 72 | debugDownload && console.log('fail') 73 | }) 74 | 75 | debugDownload && console.log('Downloading: ', url) 76 | 77 | dl.start() 78 | 79 | return dl 80 | } 81 | 82 | -------------------------------------------------------------------------------- /src/renderer/store/modules/player.js: -------------------------------------------------------------------------------- 1 | import music from '../../utils/music' 2 | 3 | // state 4 | const state = { 5 | list: [], 6 | listId: null, 7 | playIndex: -1, 8 | changePlay: false, 9 | } 10 | 11 | let urlRequest 12 | let picRequest 13 | let lrcRequest 14 | 15 | // getters 16 | const getters = { 17 | list: state => state.list || [], 18 | listId: state => state.listId, 19 | changePlay: satte => satte.changePlay, 20 | playIndex: state => state.playIndex, 21 | } 22 | 23 | // actions 24 | const actions = { 25 | getUrl({ commit, state }, { musicInfo, type, isRefresh }) { 26 | if (!musicInfo._types[type]) { 27 | // 兼容旧版酷我源搜索列表过滤128k音质的bug 28 | if (!(musicInfo.source == 'kw' && type == '128k')) return Promise.reject(new Error('该歌曲没有可播放的音频')) 29 | 30 | // return Promise.reject(new Error('该歌曲没有可播放的音频')) 31 | } 32 | if (urlRequest && urlRequest.cancelHttp) urlRequest.cancelHttp() 33 | if (musicInfo.typeUrl[type] && !isRefresh) return Promise.resolve() 34 | urlRequest = music[musicInfo.source].getMusicUrl(musicInfo, type) 35 | return urlRequest.promise.then(result => { 36 | commit('setUrl', { musicInfo, url: result.url, type }) 37 | }).finally(() => { 38 | urlRequest = null 39 | }) 40 | }, 41 | getPic({ commit, state }, musicInfo) { 42 | if (picRequest && picRequest.cancelHttp) picRequest.cancelHttp() 43 | picRequest = music[musicInfo.source].getPic(musicInfo) 44 | return picRequest.promise.then(url => { 45 | commit('getPic', { musicInfo, url }) 46 | }).finally(() => { 47 | picRequest = null 48 | }) 49 | }, 50 | getLrc({ commit, state }, musicInfo) { 51 | if (lrcRequest && lrcRequest.cancelHttp) lrcRequest.cancelHttp() 52 | lrcRequest = music[musicInfo.source].getLyric(musicInfo) 53 | return lrcRequest.promise.then(lrc => { 54 | commit('setLrc', { musicInfo, lrc }) 55 | }).finally(() => { 56 | lrcRequest = null 57 | }) 58 | }, 59 | } 60 | 61 | 62 | // mitations 63 | const mutations = { 64 | setUrl(state, datas) { 65 | datas.musicInfo.typeUrl = Object.assign({}, datas.musicInfo.typeUrl, { [datas.type]: datas.url }) 66 | }, 67 | getPic(state, datas) { 68 | datas.musicInfo.img = datas.url 69 | }, 70 | setLrc(state, datas) { 71 | datas.musicInfo.lrc = datas.lrc 72 | }, 73 | setList(state, { list, listId, index }) { 74 | state.list = list 75 | state.listId = listId 76 | state.playIndex = index 77 | state.changePlay = true 78 | }, 79 | setPlayIndex(state, index) { 80 | state.playIndex = index 81 | state.changePlay = true 82 | // console.log(state.changePlay) 83 | }, 84 | fixPlayIndex(state, index) { 85 | state.playIndex = index 86 | }, 87 | resetChangePlay(state) { 88 | state.changePlay = false 89 | }, 90 | } 91 | 92 | export default { 93 | namespaced: true, 94 | state, 95 | getters, 96 | actions, 97 | mutations, 98 | } 99 | -------------------------------------------------------------------------------- /src/renderer/store/modules/list.js: -------------------------------------------------------------------------------- 1 | // state 2 | const state = { 3 | defaultList: { 4 | id: 'default', 5 | name: '试听列表', 6 | list: [], 7 | }, 8 | loveList: { 9 | id: 'love', 10 | name: '我的收藏', 11 | list: [], 12 | }, 13 | userList: [], 14 | } 15 | 16 | // getters 17 | const getters = { 18 | defaultList: state => state.defaultList || {}, 19 | loveList: state => state.loveList || {}, 20 | userList: state => state.userList, 21 | } 22 | 23 | // actions 24 | const actions = { 25 | 26 | } 27 | 28 | const getList = (state, id) => { 29 | let targetList 30 | switch (id) { 31 | case 'default': 32 | targetList = state.defaultList 33 | break 34 | case 'love': 35 | targetList = state.loveList 36 | break 37 | default: 38 | targetList = state.userList.find(l => l.id === id) 39 | break 40 | } 41 | return targetList 42 | } 43 | 44 | // mitations 45 | const mutations = { 46 | initList(state, { defaultList, loveList }) { 47 | if (defaultList !== undefined) state.defaultList.list = defaultList.list 48 | if (loveList !== undefined) state.loveList.list = loveList.list 49 | }, 50 | setList(state, { id, list }) { 51 | const targetList = getList(state, id) 52 | if (!targetList) return 53 | targetList.list = list 54 | }, 55 | listAdd(state, { id, musicInfo }) { 56 | const targetList = getList(state, id) 57 | if (!targetList) return 58 | if (targetList.list.some(s => s.songmid === musicInfo.songmid)) return 59 | targetList.list.push(musicInfo) 60 | }, 61 | listAddMultiple(state, { id, list }) { 62 | let targetList = getList(state, id) 63 | if (!targetList) return 64 | targetList = targetList.list 65 | list.forEach(musicInfo => { 66 | if (targetList.some(s => s.songmid === musicInfo.songmid)) return 67 | targetList.push(musicInfo) 68 | }) 69 | }, 70 | listRemove(state, { id, index }) { 71 | let targetList = getList(state, id) 72 | if (!targetList) return 73 | targetList.list.splice(index, 1) 74 | }, 75 | listRemoveMultiple(state, { id, list }) { 76 | let targetList = getList(state, id) 77 | if (!targetList) return 78 | targetList = targetList.list 79 | list.forEach(musicInfo => { 80 | let index = targetList.indexOf(musicInfo) 81 | if (index < 0) return 82 | targetList.splice(index, 1) 83 | }) 84 | }, 85 | listClear(state, id) { 86 | let targetList = getList(state, id) 87 | if (!targetList) return 88 | targetList.list.length = 0 89 | }, 90 | updateMusicInfo(state, { id, index, data }) { 91 | let targetList = getList(state, id) 92 | if (!targetList) return 93 | Object.assign(targetList.list[index], data) 94 | }, 95 | } 96 | 97 | export default { 98 | namespaced: true, 99 | state, 100 | getters, 101 | actions, 102 | mutations, 103 | } 104 | -------------------------------------------------------------------------------- /src/renderer/store/state.js: -------------------------------------------------------------------------------- 1 | 2 | // const isDev = process.env.NODE_ENV === 'development' 3 | import Store from 'electron-store' 4 | import { updateSetting } from '../utils' 5 | import { windowSizeList } from '../../common/config' 6 | import { version } from '../../../package.json' 7 | const electronStore_list = window.electronStore_list = new Store({ 8 | name: 'playList', 9 | }) 10 | const electronStore_config = window.electronStore_config = new Store({ 11 | name: 'config', 12 | }) 13 | if (!electronStore_config.get('version') && electronStore_config.get('setting')) { // 迁移配置 14 | electronStore_config.set('version', electronStore_config.get('setting.version')) 15 | electronStore_config.delete('setting.version') 16 | const list = electronStore_config.get('list') 17 | if (list) { 18 | if (list.defaultList) electronStore_list.set('defaultList', list.defaultList) 19 | if (list.loveList) electronStore_list.set('loveList', list.loveList) 20 | electronStore_config.delete('list') 21 | } 22 | const downloadList = electronStore_config.get('download') 23 | if (downloadList) { 24 | if (downloadList.list) electronStore_list.set('downloadList', downloadList.list) 25 | electronStore_config.delete('download') 26 | } 27 | } 28 | const { version: settingVersion, setting } = updateSetting(electronStore_config.get('setting'), electronStore_config.get('version')) 29 | electronStore_config.set('version', settingVersion) 30 | electronStore_config.set('setting', setting) 31 | process.versions.app = version 32 | 33 | export default { 34 | themes: [ 35 | { 36 | id: 0, 37 | name: '绿意盎然', 38 | class: 'green', 39 | }, 40 | { 41 | id: 1, 42 | name: '蓝田生玉', 43 | class: 'blue', 44 | }, 45 | { 46 | id: 2, 47 | name: '信口雌黄', 48 | class: 'yellow', 49 | }, 50 | { 51 | id: 3, 52 | name: '橙黄橘绿', 53 | class: 'orange', 54 | }, 55 | { 56 | id: 4, 57 | name: '热情似火', 58 | class: 'red', 59 | }, 60 | { 61 | id: 5, 62 | name: '重斤球紫', 63 | class: 'purple', 64 | }, 65 | { 66 | id: 6, 67 | name: '灰常美丽', 68 | class: 'grey', 69 | }, 70 | { 71 | id: 7, 72 | name: '月里嫦娥', 73 | class: 'midAutumn', 74 | }, 75 | { 76 | id: 8, 77 | name: '木叶之村', 78 | class: 'dhHyrz', 79 | }, 80 | { 81 | id: 9, 82 | name: '新年快乐', 83 | class: 'happyNewYear', 84 | }, 85 | ], 86 | version: { 87 | version, 88 | newVersion: null, 89 | showModal: false, 90 | isError: false, 91 | isTimeOut: false, 92 | isUnknow: false, 93 | isDownloaded: false, 94 | isDownloading: false, 95 | isLatestVer: false, 96 | downloadProgress: null, 97 | }, 98 | userInfo: null, 99 | setting, 100 | settingVersion, 101 | 102 | windowSizeList, 103 | } 104 | -------------------------------------------------------------------------------- /src/renderer/utils/music/bd/musicSearch.js: -------------------------------------------------------------------------------- 1 | // import '../../polyfill/array.find' 2 | // import jshtmlencode from 'js-htmlencode' 3 | import { httpFetch } from '../../request' 4 | import { formatPlayTime } from '../../index' 5 | // import { debug } from '../../utils/env' 6 | // import { formatSinger } from './util' 7 | 8 | let searchRequest 9 | export default { 10 | limit: 30, 11 | total: 0, 12 | page: 0, 13 | allPage: 1, 14 | musicSearch(str, page) { 15 | if (searchRequest && searchRequest.cancelHttp) searchRequest.cancelHttp() 16 | searchRequest = httpFetch(`http://tingapi.ting.baidu.com/v1/restserver/ting?from=android&version=5.6.5.6&method=baidu.ting.search.merge&format=json&query=${encodeURIComponent(str)}&page_no=${page}&page_size=${this.limit}&type=0&data_source=0&use_cluster=1`) 17 | return searchRequest.promise.then(({ body }) => body) 18 | }, 19 | handleResult(rawData) { 20 | let ids = new Set() 21 | const list = [] 22 | if (!rawData) return list 23 | rawData.forEach(item => { 24 | if (ids.has(item.song_id)) return 25 | ids.add(item.song_id) 26 | const types = [] 27 | const _types = {} 28 | let size = null 29 | let itemTypes = item.all_rate.split(',') 30 | if (itemTypes.includes('128')) { 31 | types.push({ type: '128k', size }) 32 | _types['128k'] = { 33 | size, 34 | } 35 | } 36 | if (itemTypes.includes('320')) { 37 | types.push({ type: '320k', size }) 38 | _types['320k'] = { 39 | size, 40 | } 41 | } 42 | if (itemTypes.includes('flac')) { 43 | types.push({ type: 'flac', size }) 44 | _types.flac = { 45 | size, 46 | } 47 | } 48 | // types.reverse() 49 | 50 | list.push({ 51 | singer: item.author.replace(',', '、'), 52 | name: item.title, 53 | albumName: item.album_title, 54 | albumId: item.album_id, 55 | source: 'bd', 56 | interval: formatPlayTime(parseInt(item.file_duration)), 57 | songmid: item.song_id, 58 | img: null, 59 | lrc: null, 60 | types, 61 | _types, 62 | typeUrl: {}, 63 | }) 64 | }) 65 | return list 66 | }, 67 | search(str, page = 1, { limit } = {}, retryNum = 0) { 68 | if (++retryNum > 3) return Promise.reject(new Error('try max num')) 69 | if (limit != null) this.limit = limit 70 | 71 | return this.musicSearch(str, page).then(result => { 72 | if (!result || result.error_code !== 22000) return this.search(str, page, { limit }, retryNum) 73 | let list = this.handleResult(result.result.song_info.song_list) 74 | 75 | if (list == null) return this.search(str, page, { limit }, retryNum) 76 | 77 | this.total = result.result.song_info.total 78 | this.page = page 79 | this.allPage = Math.ceil(this.total / this.limit) 80 | 81 | return Promise.resolve({ 82 | list, 83 | allPage: this.allPage, 84 | limit: this.limit, 85 | total: this.total, 86 | source: 'bd', 87 | }) 88 | }) 89 | }, 90 | } 91 | -------------------------------------------------------------------------------- /src/renderer/utils/music/kg/musicSearch.js: -------------------------------------------------------------------------------- 1 | // import '../../polyfill/array.find' 2 | // import jshtmlencode from 'js-htmlencode' 3 | import { httpFetch } from '../../request' 4 | import { formatPlayTime, sizeFormate } from '../../index' 5 | // import { debug } from '../../utils/env' 6 | // import { formatSinger } from './util' 7 | 8 | let searchRequest 9 | export default { 10 | limit: 30, 11 | total: 0, 12 | page: 0, 13 | allPage: 1, 14 | musicSearch(str, page) { 15 | if (searchRequest && searchRequest.cancelHttp) searchRequest.cancelHttp() 16 | searchRequest = httpFetch(`http://ioscdn.kugou.com/api/v3/search/song?keyword=${encodeURIComponent(str)}&page=${page}&pagesize=${this.limit}&showtype=10&plat=2&version=7910&tag=1&correct=1&privilege=1&sver=5`) 17 | return searchRequest.promise.then(({ body }) => body) 18 | }, 19 | handleResult(rawData) { 20 | // console.log(rawData) 21 | let ids = new Set() 22 | const list = [] 23 | rawData.forEach(item => { 24 | if (ids.has(item.audio_id)) return 25 | ids.add(item.audio_id) 26 | const types = [] 27 | const _types = {} 28 | if (item.filesize !== 0) { 29 | let size = sizeFormate(item.filesize) 30 | types.push({ type: '128k', size, hash: item.hash }) 31 | _types['128k'] = { 32 | size, 33 | hash: item.hash, 34 | } 35 | } 36 | if (item['320filesize'] !== 0) { 37 | let size = sizeFormate(item['320filesize']) 38 | types.push({ type: '320k', size, hash: item['320hash'] }) 39 | _types['320k'] = { 40 | size, 41 | hash: item['320hash'], 42 | } 43 | } 44 | if (item.sqfilesize !== 0) { 45 | let size = sizeFormate(item.sqfilesize) 46 | types.push({ type: 'flac', size, hash: item.sqhash }) 47 | _types.flac = { 48 | size, 49 | hash: item.sqhash, 50 | } 51 | } 52 | list.push({ 53 | singer: item.singername, 54 | name: item.songname, 55 | albumName: item.album_name, 56 | albumId: item.album_id, 57 | songmid: item.audio_id, 58 | source: 'kg', 59 | interval: formatPlayTime(item.duration), 60 | _interval: item.duration, 61 | img: null, 62 | lrc: null, 63 | hash: item.hash, 64 | types, 65 | _types, 66 | typeUrl: {}, 67 | }) 68 | }) 69 | return list 70 | }, 71 | search(str, page = 1, { limit } = {}, retryNum = 0) { 72 | if (++retryNum > 3) return Promise.reject(new Error('try max num')) 73 | if (limit != null) this.limit = limit 74 | // http://newlyric.kuwo.cn/newlyric.lrc?62355680 75 | return this.musicSearch(str, page).then(result => { 76 | if (!result || result.errcode !== 0) return this.search(str, page, { limit }, retryNum) 77 | let list = this.handleResult(result.data.info) 78 | 79 | if (list == null) return this.search(str, page, { limit }, retryNum) 80 | 81 | this.total = result.data.total 82 | this.page = page 83 | this.allPage = Math.ceil(this.total / this.limit) 84 | 85 | return Promise.resolve({ 86 | list, 87 | allPage: this.allPage, 88 | limit: this.limit, 89 | total: this.total, 90 | source: 'kg', 91 | }) 92 | }) 93 | }, 94 | } 95 | -------------------------------------------------------------------------------- /src/renderer/utils/music/wy/musicSearch.js: -------------------------------------------------------------------------------- 1 | import { httpFetch } from '../../request' 2 | import { weapi } from './utils/crypto' 3 | import { sizeFormate, formatPlayTime } from '../../index' 4 | 5 | let searchRequest 6 | export default { 7 | limit: 30, 8 | total: 0, 9 | page: 0, 10 | allPage: 1, 11 | musicSearch(str, page) { 12 | if (searchRequest && searchRequest.cancelHttp) searchRequest.cancelHttp() 13 | searchRequest = httpFetch('http://music.163.com/weapi/cloudsearch/get/web?csrf_token=', { 14 | method: 'post', 15 | form: weapi({ 16 | s: str, 17 | type: 1, // 1: 单曲, 10: 专辑, 100: 歌手, 1000: 歌单, 1002: 用户, 1004: MV, 1006: 歌词, 1009: 电台, 1014: 视频 18 | limit: this.limit, 19 | offset: this.limit * (page - 1), 20 | }), 21 | }) 22 | return searchRequest.promise.then(({ body }) => body) 23 | }, 24 | getSinger(singers) { 25 | let arr = [] 26 | singers.forEach(singer => { 27 | arr.push(singer.name) 28 | }) 29 | return arr.join('、') 30 | }, 31 | handleResult(rawList) { 32 | // console.log(rawList) 33 | return rawList.map(item => { 34 | const types = [] 35 | const _types = {} 36 | let size 37 | switch (item.privilege.maxbr) { 38 | case 999000: 39 | size = null 40 | types.push({ type: 'flac', size }) 41 | _types.flac = { 42 | size, 43 | } 44 | case 320000: 45 | if (item.h) { 46 | size = sizeFormate(item.h.size) 47 | types.push({ type: '320k', size }) 48 | _types['320k'] = { 49 | size, 50 | } 51 | } 52 | case 128000: 53 | if (item.l) { 54 | size = sizeFormate(item.l.size) 55 | types.push({ type: '128k', size }) 56 | _types['128k'] = { 57 | size, 58 | } 59 | } 60 | } 61 | 62 | types.reverse() 63 | 64 | return { 65 | singer: this.getSinger(item.ar), 66 | name: item.name, 67 | albumName: item.al.name, 68 | albumId: item.al.id, 69 | source: 'wy', 70 | interval: formatPlayTime(item.dt / 1000), 71 | songmid: item.id, 72 | img: item.al.picUrl, 73 | lrc: null, 74 | types, 75 | _types, 76 | typeUrl: {}, 77 | } 78 | }) 79 | }, 80 | search(str, page = 1, { limit } = {}, retryNum = 0) { 81 | if (++retryNum > 3) return Promise.reject(new Error('try max num')) 82 | if (limit != null) this.limit = limit 83 | return this.musicSearch(str, page).then(result => { 84 | // console.log(JSON.stringify(result)) 85 | if (!result || result.code !== 200) return this.search(str, page, { limit }, retryNum) 86 | let list = this.handleResult(result.result.songs) 87 | 88 | if (list == null) return this.search(str, page, { limit }, retryNum) 89 | 90 | this.total = result.result.songCount 91 | this.page = page 92 | this.allPage = Math.ceil(this.total / this.limit) 93 | 94 | return Promise.resolve({ 95 | list, 96 | allPage: this.allPage, 97 | limit: this.limit, 98 | total: this.total, 99 | source: 'wy', 100 | }) 101 | }) 102 | }, 103 | } 104 | -------------------------------------------------------------------------------- /src/renderer/utils/music/kw/index.js: -------------------------------------------------------------------------------- 1 | import { httpGet, cancelHttp } from '../../request' 2 | import tempSearch from './tempSearch' 3 | import musicSearch from './musicSearch' 4 | import { formatSinger, getToken } from './util' 5 | import leaderboard from './leaderboard' 6 | import lyric from './lyric' 7 | import pic from './pic' 8 | import api_source from '../api-source' 9 | import songList from './songList' 10 | 11 | const kw = { 12 | _musicInfoRequestObj: null, 13 | _musicInfoPromiseCancelFn: null, 14 | _musicPicRequestObj: null, 15 | _musicPicPromiseCancelFn: null, 16 | // context: null, 17 | 18 | 19 | // init(context) { 20 | // if (this.isInited) return 21 | // this.isInited = true 22 | // this.context = context 23 | 24 | // // this.musicSearch.search('我又想你了').then(res => { 25 | // // console.log(res) 26 | // // }) 27 | 28 | // // this.getMusicUrl('62355680', '320k').then(url => { 29 | // // console.log(url) 30 | // // }) 31 | // }, 32 | 33 | tempSearch, 34 | musicSearch, 35 | leaderboard, 36 | songList, 37 | getLyric(songInfo) { 38 | // let singer = songInfo.singer.indexOf('、') > -1 ? songInfo.singer.split('、')[0] : songInfo.singer 39 | return lyric.getLyric(songInfo.songmid) 40 | }, 41 | handleMusicInfo(songInfo) { 42 | return this.getMusicInfo(songInfo).then(info => { 43 | // console.log(JSON.stringify(info)) 44 | songInfo.name = info.name 45 | songInfo.singer = formatSinger(info.artist) 46 | songInfo.img = info.pic 47 | songInfo.albumName = info.album 48 | return songInfo 49 | // return Object.assign({}, songInfo, { 50 | // name: info.name, 51 | // singer: formatSinger(info.artist), 52 | // img: info.pic, 53 | // albumName: info.album, 54 | // }) 55 | }) 56 | }, 57 | 58 | getMusicUrl(songInfo, type) { 59 | return api_source('kw').getMusicUrl(songInfo, type) 60 | }, 61 | 62 | getMusicInfo(songInfo) { 63 | if (this._musicInfoRequestObj != null) { 64 | cancelHttp(this._musicInfoRequestObj) 65 | this._musicInfoPromiseCancelFn(new Error('取消http请求')) 66 | } 67 | return new Promise((resolve, reject) => { 68 | this._musicInfoPromiseCancelFn = reject 69 | this._musicInfoRequestObj = httpGet(`http://www.kuwo.cn/api/www/music/musicInfo?mid=${songInfo.songmid}`, (err, resp, body) => { 70 | this._musicInfoRequestObj = null 71 | this._musicInfoPromiseCancelFn = null 72 | if (err) { 73 | console.log(err) 74 | reject(err) 75 | } 76 | body.code === 200 ? resolve(body.data) : reject(new Error(body.msg)) 77 | }) 78 | }) 79 | }, 80 | 81 | getMusicUrls(musicInfo, cb) { 82 | let tasks = [] 83 | let songId = musicInfo.songmid 84 | musicInfo.types.forEach(type => { 85 | tasks.push(kw.getMusicUrl(songId, type.type).promise) 86 | }) 87 | Promise.all(tasks).then(urlInfo => { 88 | let typeUrl = {} 89 | urlInfo.forEach(info => { 90 | typeUrl[info.type] = info.url 91 | }) 92 | cb(typeUrl) 93 | }) 94 | }, 95 | 96 | getPic(songInfo) { 97 | return pic.getPic(songInfo) 98 | }, 99 | 100 | init() { 101 | getToken() 102 | }, 103 | } 104 | 105 | export default kw 106 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # lx-music-desktop 常见问题 2 | 3 | 在阅读本常见问题后,仍然无法解决你的问题,请提交issue或者加企鹅群`830125506`反馈,反馈时请**注明**已阅读常见问题! 4 | 5 | ## 软件为什么没有桌面歌词与自定义列表功能 6 | 7 | 洛雪音乐的最初定位不是作为播放器开发的,它主要用于**查找歌曲**,软件的播放功能仅用于试听,不建议用作为常用播放器使用,因此无桌面、界面歌词,不可自定义列表等功能。 8 | 9 | ## 歌曲无法试听与下载 10 | 11 | 该问题解决顺序如下: 12 | 13 | 1. 尝试更新到最新版本 14 | 2. 尝试切换其他歌曲(或直接搜索该歌曲),若全部歌曲都无法试听与下载则进行下一步 15 | 3. 尝试到 设置-音乐来源 切换到其他接口 16 | 4. 尝试切换网络,比如用手机开热点(目前存在某些网络无法访问接口服务器的情况) 17 | 5. 若还不行请到这个链接查看详情: 18 | 6. 若没有在第5条链接中的第一条评论中看到接口无法使用的说明,则应该是你网络无法访问接口服务器的问题,如果接口有问题我会在那里说明。 19 | 20 | 想要知道是不是自己网络的问题可以看看`https://ts.tempmusic.tk`能不能在浏览器打开,浏览器显示404是正常的,如果不是404那就证明所在网络无法访问接口服务器。 21 | 若网页无法打开或打来不是404,则应该是DNS的问题,可以尝试以下办法: 22 | 23 | 1. 改成自动获取试试 24 | 2. 手动把DNS改一下,不要用360的DNS,可以把DNS改成`114.114.114.114`、`8.8.8.8` 25 | 3. 软件设置里面的代理不要勾起来 26 | 27 | ## 软件安装包说明 28 | 29 | 软件发布页及网盘中有多个类型的安装文件,以下是对这些类型文件的说明: 30 | 31 | 文件名带 `win_` 的是在Windows系统上运行的版本,
32 | 其中安装版(Setup)可自动更新软件,
33 | 绿色版(green)为免安装版,自动更新功能不可用; 34 | 35 | 以 **`.dmg`** 结尾的文件为 MAC 版本; 36 | 37 | 以 **`.AppImage`**、**`.deb`** 结尾的为 Linux 版本。 38 | 39 | 带有`x64`的为64位的系统版本,带`x86`的为32位的系统版本;若两个都带有的则为集合版,安装时会自动根据系统位数选择对应的版本安装。 40 | 41 | ## 软件更新 42 | 43 | 软件启动时若发现新版本时会自动从本仓库下载安装包,下载完毕会弹窗提示更新。
44 | 若下载未完成时软件被关闭,下次启动软件会再次自动下载。
45 | 若还是**更新失败**,可能是无法访问GitHub导致的,这时需要手动更新,即下载最新安装包直接覆盖安装即可。
46 | 注意:**绿色版**的软件自动更新功能**不可用**,建议使用安装版!!
47 | 注意:**Mac版**、**Linux deb**版不支持自动更新! 48 | 49 | ## Windows 7 下界面异常 50 | 51 | 由于软件默认使用了透明窗口,根据Electron官方文档的[说明](https://electronjs.org/docs/api/frameless-window#%E5%B1%80%E9%99%90%E6%80%A7): 52 | > 在 windows 操作系统上, 当 DWM 被禁用时, 透明窗口将无法工作。 53 | 54 | 因此,当 win7 没有开启**透明效果**时界面将会显示异常,开启方法请自行百度(开启后可看到任务栏变透明)。
55 | 从`0.14.0`版本起不再强制要求开启透明效果,若你实在不想开启(若非电脑配置太低,墙裂建议开启!),可通过添加运行参数`-nt`来运行程序即可,例如:`.\lx-music-desktop.exe -nt`,添加方法可自行百度“给快捷方式加参数”,该参数的作用是用来控制程序是否使用非透明窗口运行。 56 | 57 | 对于一些完全无法正常显示界面的情况,请阅读下面的 **软件启动后,界面无法显示** 58 | 59 | ## 软件启动后,界面无法显示 60 | 61 | 软件启动后,可以在任务栏看到软件,但软件界面在桌面上无任何显示。
62 | 原始问题看:
63 | 解决办法:下载`.NET Framework 4.7.1`或**更高**版本安装即可(建议安装最新版,若安装过程中遇到问题可尝试自行百度解决)。
64 | 微软官方下载地址:
65 | 下载`Runtime(运行时)`版即可,安装完成后可能需要重启才生效。 66 | 67 | 若还是不行可尝试以下操作: 68 | 69 | - 更新显卡驱动 70 | - 尝试将绿色版的软件放在**桌面**或**我的文档**运行 71 | 72 | ## 安装版安装失败,提示安装程序并未成功地运行完成 73 | 74 | 对于部分电脑出现安装失败的问题,可以做出以下尝试: 75 | 76 | - 若你之前可以安装成功,但现在安装失败,就去**控制面板-程序和功能**或用第三方卸载工具看下有没有之前的版本残留,若同时在不同路径下安装了多个版本就可能会出现该问题,这种情况卸载掉所有版本重新安装即可 77 | - 清理安装路径下的残留文件 78 | - 以管理员权限打开`cmd`,输入`sfc /scannow`回车等待检查完成重启电脑 79 | - 若还是不行我也没办法了。。 80 | 81 | ## 缺少`xxx.dll` 82 | 83 | 这个是电脑缺少某些dll导致的,正常的系统是没有这个问题的,可以尝试如下几个解决办法: 84 | 85 | - 以管理员权限打开`cmd`,输入`sfc /scannow`回车等待检查完成重启电脑 86 | - 若上面的方法**修复、重启**电脑后仍然不行,就自行百度弹出的**错误信息**看下别人是怎么解决的 87 | 88 | ## 杀毒软件提示有病毒或恶意行为 89 | 90 | 本人只能保证我写的代码不包含任何**恶意代码**、**收集用户信息**的行为,并且软件代码已开源,请自行查阅,软件安装包也是由CI拉取源代码构建,构建日志:[windows包](https://ci.appveyor.com/project/lyswhut/lx-music-desktop)、[Mac/Linux包](https://travis-ci.org/lyswhut/lx-music-desktop)
91 | 尽管如此,但这不意味着软件是100%安全的,由于软件使用了第三方依赖,当这些依赖存在恶意行为时(供应链攻击),软件也将会受到牵连,所以我只能尽量选择使用较多人用、信任度较高的依赖。
92 | 当然,以上说明建立的前提是在你所用的安装包是从**本项目主页上写的链接**下载的,或者有相关能力者还可以下载源代码自己构建安装包。 93 | 94 | 最后,若出现杀毒软件报毒,请自行判断选择是否继续使用本软件! 95 | 96 | ## 软件无法联网 97 | 98 | 软件的排行榜、歌单、搜索列表**都**无法加载: 99 | 100 | - 检查是否在设置界面开启了代理(当代理乱设置时软件将无法联网) 101 | - 检查软件是否被第三方软件/防火墙阻止联网 102 | -------------------------------------------------------------------------------- /src/renderer/store/modules/search.js: -------------------------------------------------------------------------------- 1 | import music from '../../utils/music' 2 | const sources = [] 3 | const sourceList = {} 4 | const sourceMaxPage = {} 5 | for (const source of music.sources) { 6 | const musicSearch = music[source.id].musicSearch 7 | if (!musicSearch) continue 8 | sources.push(source) 9 | sourceList[source.id] = { 10 | page: 1, 11 | allPage: 0, 12 | limit: 30, 13 | total: 0, 14 | list: [], 15 | } 16 | sourceMaxPage[source.id] = 0 17 | } 18 | 19 | sources.push({ 20 | id: 'all', 21 | name: '聚合搜索', 22 | }) 23 | 24 | // state 25 | const state = { 26 | sourceList, 27 | list: [], 28 | text: '', 29 | page: 1, 30 | limit: 30, 31 | allPage: 1, 32 | total: 0, 33 | sourceMaxPage, 34 | } 35 | 36 | // getters 37 | const getters = { 38 | sources: () => sources, 39 | sourceList: state => state.sourceList || [], 40 | searchText: state => state.text, 41 | allList: state => ({ list: state.list, allPage: state.allPage, page: state.page, total: state.total, limit: state.limit, sourceMaxPage: state.sourceMaxPage }), 42 | } 43 | 44 | // actions 45 | const actions = { 46 | search({ commit, rootState }, { text, page, limit }) { 47 | if (rootState.setting.search.searchSource == 'all') { 48 | let task = [] 49 | for (const source of sources) { 50 | if (source.id == 'all') continue 51 | task.push(music[source.id].musicSearch.search(text, page)) 52 | } 53 | Promise.all(task).then(results => commit('setLists', { results, text, page })) 54 | } else { 55 | return music[rootState.setting.search.searchSource].musicSearch.search(text, page, limit) 56 | .then(data => commit('setList', { text, page, ...data })) 57 | } 58 | }, 59 | } 60 | 61 | // mitations 62 | const mutations = { 63 | setList(state, datas) { 64 | let source = state.sourceList[datas.source] 65 | source.list = datas.list 66 | source.total = datas.total 67 | source.allPage = datas.allPage 68 | source.page = datas.page 69 | source.limit = datas.limit 70 | state.text = datas.text 71 | }, 72 | setLists(state, { results, text, page }) { 73 | let pages = [] 74 | let total = 0 75 | let limit = 0 76 | let list = [] 77 | for (const source of results) { 78 | state.sourceMaxPage[source.source] = source.allPage 79 | if (source.allPage < page) continue 80 | list.push(...source.list) 81 | pages.push(source.allPage) 82 | total += source.total 83 | limit += source.limit 84 | } 85 | list.sort((a, b) => b.name.charCodeAt(0) - a.name.charCodeAt(0)) 86 | state.allPage = Math.max(...pages) 87 | state.total = total 88 | state.limit = limit 89 | state.page = page 90 | state.text = text 91 | state.list = list 92 | }, 93 | clearList(state) { 94 | for (const source of Object.keys(state.sourceList)) { 95 | state.sourceList[source].list.length = 0 96 | state.sourceList[source].page = 0 97 | state.sourceList[source].allPage = 0 98 | state.sourceList[source].total = 0 99 | state.sourceMaxPage[source] = 0 100 | } 101 | state.list.length = 0 102 | state.page = 0 103 | state.allPage = 0 104 | state.total = 0 105 | state.text = '' 106 | }, 107 | } 108 | 109 | export default { 110 | namespaced: true, 111 | state, 112 | getters, 113 | actions, 114 | mutations, 115 | } 116 | -------------------------------------------------------------------------------- /src/renderer/utils/music/mg/leaderboard.js: -------------------------------------------------------------------------------- 1 | import { httpFetch } from '../../request' 2 | // import { formatPlayTime } from '../../index' 3 | // import jshtmlencode from 'js-htmlencode' 4 | 5 | export default { 6 | limit: 200, 7 | list: [ 8 | { 9 | id: 'mgyyb', 10 | name: '音乐榜', 11 | bangid: '23603703', 12 | }, 13 | { 14 | id: 'mgysb', 15 | name: '影视榜', 16 | bangid: '23603721', 17 | }, 18 | { 19 | id: 'mghybnd', 20 | name: '华语内地榜', 21 | bangid: '23603926', 22 | }, 23 | { 24 | id: 'mghyjqbgt', 25 | name: '华语港台榜', 26 | bangid: '23603954', 27 | }, 28 | { 29 | id: 'mgomb', 30 | name: '欧美榜', 31 | bangid: '23603974', 32 | }, 33 | { 34 | id: 'mgrhb', 35 | name: '日韩榜', 36 | bangid: '23603982', 37 | }, 38 | { 39 | id: 'mgwlb', 40 | name: '网络榜', 41 | bangid: '23604058', 42 | }, 43 | { 44 | id: 'mgclb', 45 | name: '彩铃榜', 46 | bangid: '23604023', 47 | }, 48 | { 49 | id: 'mgktvb', 50 | name: 'KTV榜', 51 | bangid: '23604040', 52 | }, 53 | { 54 | id: 'mgrcb', 55 | name: '原创榜', 56 | bangid: '23604032', 57 | }, 58 | ], 59 | getUrl(id, page) { 60 | return `http://m.music.migu.cn/migu/remoting/cms_list_tag?nid=${id}&pageSize=${this.limit}&pageNo=${page - 1}` 61 | }, 62 | requestObj: null, 63 | getData(url) { 64 | if (this.requestObj) this.requestObj.cancelHttp() 65 | this.requestObj = httpFetch(url) 66 | return this.requestObj.promise 67 | }, 68 | filterData(rawList) { 69 | // console.log(rawList) 70 | let ids = new Set() 71 | const list = [] 72 | rawList.forEach(({ songData }) => { 73 | if (!songData) return 74 | if (ids.has(songData.copyrightId)) return 75 | ids.add(songData.copyrightId) 76 | 77 | const types = [] 78 | const _types = {} 79 | let size = null 80 | types.push({ type: '128k', size }) 81 | _types['128k'] = { 82 | size, 83 | } 84 | 85 | if (songData.hasHQqq === '1') { 86 | types.push({ type: '320k', size }) 87 | _types['320k'] = { 88 | size, 89 | } 90 | } 91 | if (songData.hasSQqq === '1') { 92 | types.push({ type: 'flac', size }) 93 | _types.flac = { 94 | size, 95 | } 96 | } 97 | // types.reverse() 98 | 99 | list.push({ 100 | singer: songData.singerName.join('、'), 101 | name: songData.songName, 102 | // albumName: songData.album_title, 103 | // albumId: songData.album_id, 104 | source: 'mg', 105 | interval: null, 106 | songmid: songData.copyrightId, 107 | copyrightId: songData.copyrightId, 108 | img: songData.picL || songData.M || songData.picS, 109 | lrc: null, 110 | types, 111 | _types, 112 | typeUrl: {}, 113 | }) 114 | }) 115 | 116 | return list 117 | }, 118 | getList(id, page) { 119 | let type = this.list.find(s => s.id === id) 120 | if (!type) return Promise.reject() 121 | return this.getData(this.getUrl(type.bangid, page)).then(({ statusCode, body }) => { 122 | if (statusCode !== 200) return Promise.reject(new Error('获取列表失败')) 123 | const list = this.filterData(body.result.results) 124 | return { 125 | total: body.result.totalCount, 126 | list, 127 | limit: body.result.pageSize, 128 | page, 129 | source: 'mg', 130 | } 131 | }) 132 | }, 133 | } 134 | -------------------------------------------------------------------------------- /src/renderer/utils/music/bd/leaderboard.js: -------------------------------------------------------------------------------- 1 | import { httpFetch } from '../../request' 2 | // import { formatPlayTime } from '../../index' 3 | // import jshtmlencode from 'js-htmlencode' 4 | 5 | export default { 6 | limit: 20, 7 | list: [ 8 | { 9 | id: 'bdrgb', 10 | name: '热歌榜', 11 | bangid: '2', 12 | }, 13 | { 14 | id: 'bdxgb', 15 | name: '新歌榜', 16 | bangid: '1', 17 | }, 18 | { 19 | id: 'bdycb', 20 | name: '原创榜', 21 | bangid: '200', 22 | }, 23 | { 24 | id: 'bdhyjqb', 25 | name: '华语榜', 26 | bangid: '20', 27 | }, 28 | { 29 | id: 'bdomjqb', 30 | name: '欧美榜', 31 | bangid: '21', 32 | }, 33 | { 34 | id: 'bdwugqb', 35 | name: '网络榜', 36 | bangid: '25', 37 | }, 38 | { 39 | id: 'bdjdlgb', 40 | name: '老歌榜', 41 | bangid: '22', 42 | }, 43 | { 44 | id: 'bdysjqb', 45 | name: '影视金曲榜', 46 | bangid: '24', 47 | }, 48 | { 49 | id: 'bdqgdcb', 50 | name: '情歌对唱榜', 51 | bangid: '23', 52 | }, 53 | { 54 | id: 'bdygb', 55 | name: '摇滚榜', 56 | bangid: '11', 57 | }, 58 | ], 59 | getUrl(id, p) { 60 | return `http://musicmini.qianqian.com/2018/static/bangdan/bangdanList_${id}_${p}.html` 61 | }, 62 | regExps: { 63 | item: /data-song="({.+?})"/g, 64 | info: /{total[\s:]+"(\d+)", size[\s:]+"(\d+)", page[\s:]+"(\d+)"}/, 65 | }, 66 | requestObj: null, 67 | getData(url) { 68 | if (this.requestObj) this.requestObj.cancelHttp() 69 | this.requestObj = httpFetch(url) 70 | return this.requestObj.promise 71 | }, 72 | filterData(rawList) { 73 | // console.log(rawList) 74 | return rawList.map(item => { 75 | const types = [] 76 | const _types = {} 77 | let size = null 78 | types.push({ type: '128k', size }) 79 | _types['128k'] = { 80 | size, 81 | } 82 | if (item.biaoshi) { 83 | types.push({ type: '320k', size }) 84 | _types['320k'] = { 85 | size, 86 | } 87 | types.push({ type: 'flac', size }) 88 | _types.flac = { 89 | size, 90 | } 91 | } 92 | // types.reverse() 93 | 94 | return { 95 | singer: item.song_artist.replace(',', '、'), 96 | name: item.song_title, 97 | albumName: item.album_title, 98 | albumId: item.album_id, 99 | source: 'bd', 100 | interval: '', 101 | songmid: item.song_id, 102 | img: null, 103 | lrc: null, 104 | types, 105 | _types, 106 | typeUrl: {}, 107 | } 108 | }) 109 | }, 110 | parseData(rawData) { 111 | // return rawData.map(item => JSON.parse(item.replace(this.regExps.item, '$1').replace(/"/g, '"').replace(/\\\//g, '/').replace(/(@s_1,w_)\d+(,h_)\d+/, '$1500$2500'))) 112 | return rawData.map(item => JSON.parse(item.replace(this.regExps.item, '$1').replace(/"/g, '"').replace(/\\\//g, '/'))) 113 | }, 114 | getList(id, page) { 115 | let type = this.list.find(s => s.id === id) 116 | if (!type) return Promise.reject() 117 | return this.getData(this.getUrl(type.bangid, page)).then(({ body }) => { 118 | let result = body.match(this.regExps.item) 119 | if (!result) return Promise.reject(new Error('匹配list失败')) 120 | let info = body.match(this.regExps.info) 121 | if (!info) return Promise.reject(new Error('匹配info失败')) 122 | const list = this.filterData(this.parseData(result)) 123 | this.limit = parseInt(info[2]) 124 | return { 125 | total: parseInt(info[1]), 126 | list, 127 | limit: this.limit, 128 | page: parseInt(info[3]), 129 | source: 'bd', 130 | } 131 | }) 132 | }, 133 | } 134 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

lx-music logo

2 | 3 |

4 | Release version 5 | Build status 6 | Build status 7 | Electron version 8 | 9 | Dev branch version 10 | 11 |

12 | 13 | 18 | 19 | 30 | 31 |

洛雪音乐助手桌面版

32 | 33 | ### 说明 34 | 35 | 一个基于 Electron + Vue 开发的音乐软件。 36 | 37 | 所用技术栈: 38 | 39 | - Electron 8 40 | - Vue 2 41 | 42 | 已支持的平台: 43 | 44 | - Windows 7 及以上 45 | - Mac OS 46 | - Linux 47 | 48 | 软件变化请查看:[更新日志](https://github.com/lyswhut/lx-music-desktop/blob/master/CHANGELOG.md)
49 | 软件下载请转到:[发布页面](https://github.com/lyswhut/lx-music-desktop/releases)
50 | 或者到网盘下载(网盘内有MAC、windows版):`https://www.lanzous.com/b906260/` 密码:`glqw`
51 | 使用常见问题请转至:[常见问题](https://github.com/lyswhut/lx-music-desktop/blob/master/FAQ.md) 52 | 53 | ### 源码使用方法 54 | 55 | 环境要求:Node.js 12.x 56 | 57 | ```bash 58 | # 开发模式 59 | npm run dev 60 | 61 | # 构建免安装版 62 | npm run pack:dir 63 | 64 | # 构建安装包(windows版) 65 | npm run pack 66 | 67 | ``` 68 | 69 | ### UI界面 70 | 71 |

lx-music UI

72 | 73 | ### 常见问题 74 | 75 | 常见问题已移至: 76 | 77 | ### 致谢 78 | 79 | 感谢 [@messoer](https://github.com/messoer) 曾经提供的部分音乐API! 80 | 81 | ### 免责声明 82 | 83 | 本项目**不开发或者破解直接获取音频数据**的功能,所有音频数据均来自**第三方接口**!
84 | 本软件仅用于**测试 `electron 8` 在各种系统上的兼容性**及用于**对比各大音乐平台歌单、排行榜等数据列表的差异性**,使用本软件产生的**任何涉及版权相关的数据**请于**24小时内删除**。
85 | 本软件仅用于学习交流使用,禁止用于商业用途,使用本软件所造成的的后果由使用者承担!
86 | 若对此有疑问请 mail to: lyswhut@qq.com 87 | 88 | ### 许可证 89 | 90 | [Apache License 2.0](https://github.com/lyswhut/lx-music-desktop/blob/master/LICENSE) 91 | -------------------------------------------------------------------------------- /src/main/index.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow, Menu } = require('electron') 2 | const path = require('path') 3 | 4 | // 单例应用程序 5 | if (!app.requestSingleInstanceLock()) { 6 | app.quit() 7 | return 8 | } 9 | app.on('second-instance', (event, argv, cwd) => { 10 | if (mainWindow) { 11 | if (mainWindow.isMinimized()) mainWindow.restore() 12 | mainWindow.focus() 13 | } else { 14 | app.quit() 15 | } 16 | }) 17 | 18 | const isDev = process.env.NODE_ENV !== 'production' 19 | 20 | // https://github.com/electron/electron/issues/18397 21 | app.allowRendererProcessReuse = !isDev 22 | 23 | const { getWindowSizeInfo, parseEnv } = require('./utils') 24 | 25 | global.envParams = parseEnv() 26 | 27 | require('../common/error') 28 | require('./events') 29 | const autoUpdate = require('./utils/autoUpdate') 30 | const { isLinux, isMac } = require('../common/utils') 31 | 32 | 33 | let mainWindow 34 | let winURL 35 | let isFirstCheckedUpdate = true 36 | 37 | if (isDev) { 38 | global.__static = path.join(__dirname, '../static') 39 | winURL = 'http://localhost:9080' 40 | } else { 41 | global.__static = path.join(__dirname, '/static') 42 | winURL = `file://${__dirname}/index.html` 43 | } 44 | 45 | function createWindow() { 46 | let windowSizeInfo = getWindowSizeInfo() 47 | /** 48 | * Initial window options 49 | */ 50 | mainWindow = global.mainWindow = new BrowserWindow({ 51 | height: windowSizeInfo.height, 52 | useContentSize: true, 53 | width: windowSizeInfo.width, 54 | frame: false, 55 | transparent: !isLinux && !global.envParams.nt, 56 | enableRemoteModule: false, 57 | // icon: path.join(global.__static, isWin ? 'icons/256x256.ico' : 'icons/512x512.png'), 58 | resizable: false, 59 | maximizable: false, 60 | fullscreenable: false, 61 | webPreferences: { 62 | // contextIsolation: true, 63 | webSecurity: !isDev, 64 | nodeIntegration: true, 65 | }, 66 | }) 67 | 68 | mainWindow.loadURL(winURL) 69 | 70 | mainWindow.on('close', () => { 71 | mainWindow.setProgressBar(-1) 72 | }) 73 | mainWindow.on('closed', () => { 74 | mainWindow = global.mainWindow = null 75 | }) 76 | 77 | // mainWindow.webContents.openDevTools() 78 | 79 | if (!isDev) { 80 | autoUpdate(isFirstCheckedUpdate) 81 | isFirstCheckedUpdate = false 82 | } 83 | } 84 | 85 | if (isMac) { 86 | const template = [ 87 | { 88 | label: app.getName(), 89 | submenu: [ 90 | { label: '关于洛雪音乐', role: 'about' }, 91 | { type: 'separator' }, 92 | { label: '隐藏', role: 'hide' }, 93 | { label: '显示其他', role: 'hideothers' }, 94 | { label: '显示全部', role: 'unhide' }, 95 | { type: 'separator' }, 96 | { label: '退出', accelerator: 'Command+Q', click: () => app.quit() }, 97 | ], 98 | }, 99 | { 100 | label: '窗口', 101 | role: 'window', 102 | submenu: [ 103 | { label: '最小化', role: 'minimize' }, 104 | { label: '关闭', role: 'close' }, 105 | ], 106 | }, 107 | { 108 | label: '编辑', 109 | submenu: [ 110 | { label: '撤销', accelerator: 'CmdOrCtrl+Z', role: 'undo' }, 111 | { label: '恢复', accelerator: 'Shift+CmdOrCtrl+Z', role: 'redo' }, 112 | { type: 'separator' }, 113 | { label: '剪切', accelerator: 'CmdOrCtrl+X', role: 'cut' }, 114 | { label: '复制', accelerator: 'CmdOrCtrl+C', role: 'copy' }, 115 | { label: '粘贴', accelerator: 'CmdOrCtrl+V', role: 'paste' }, 116 | { label: '选择全部', accelerator: 'CmdOrCtrl+A', role: 'selectAll' }, 117 | ], 118 | }, 119 | ] 120 | 121 | Menu.setApplicationMenu(Menu.buildFromTemplate(template)) 122 | } else { 123 | Menu.setApplicationMenu(null) 124 | } 125 | 126 | app.on('ready', createWindow) 127 | 128 | app.on('window-all-closed', () => { 129 | if (!isMac) app.quit() 130 | }) 131 | 132 | app.on('activate', () => { 133 | if (mainWindow === null) { 134 | createWindow() 135 | } 136 | }) 137 | -------------------------------------------------------------------------------- /src/renderer/utils/music/tx/musicSearch.js: -------------------------------------------------------------------------------- 1 | // import '../../polyfill/array.find' 2 | // import jshtmlencode from 'js-htmlencode' 3 | import { httpFetch } from '../../request' 4 | import { formatPlayTime, sizeFormate } from '../../index' 5 | // import { debug } from '../../utils/env' 6 | // import { formatSinger } from './util' 7 | 8 | let searchRequest 9 | export default { 10 | limit: 30, 11 | total: 0, 12 | page: 0, 13 | allPage: 1, 14 | successCode: 0, 15 | musicSearch(str, page, retryNum = 0) { 16 | if (searchRequest && searchRequest.cancelHttp) searchRequest.cancelHttp() 17 | if (retryNum > 5) return Promise.reject(new Error('搜索失败')) 18 | searchRequest = httpFetch(`https://c.y.qq.com/soso/fcgi-bin/client_search_cp?ct=24&qqmusic_ver=1298&new_json=1&remoteplace=sizer.yqq.song_next&searchid=49252838123499591&t=0&aggr=1&cr=1&catZhida=1&lossless=0&flag_qc=0&p=${page}&n=${this.limit}&w=${encodeURIComponent(str)}&loginUin=0&hostUin=0&format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq&needNewCode=0`) 19 | // searchRequest = httpFetch(`http://ioscdn.kugou.com/api/v3/search/song?keyword=${encodeURIComponent(str)}&page=${page}&pagesize=${this.limit}&showtype=10&plat=2&version=7910&tag=1&correct=1&privilege=1&sver=5`) 20 | return searchRequest.promise.then(({ body }) => { 21 | if (body.code !== this.successCode) return this.musicSearch(str, page, ++retryNum) 22 | return body.data 23 | }) 24 | }, 25 | getSinger(singers) { 26 | let arr = [] 27 | singers.forEach(singer => { 28 | arr.push(singer.name) 29 | }) 30 | return arr.join('、') 31 | }, 32 | handleResult(rawList) { 33 | // console.log(rawData) 34 | return rawList.map(item => { 35 | let types = [] 36 | let _types = {} 37 | if (item.file.size_128mp3 !== 0) { 38 | let size = sizeFormate(item.file.size_128mp3) 39 | types.push({ type: '128k', size }) 40 | _types['128k'] = { 41 | size, 42 | } 43 | } 44 | if (item.file.size_320mp3 !== 0) { 45 | let size = sizeFormate(item.file.size_320mp3) 46 | types.push({ type: '320k', size }) 47 | _types['320k'] = { 48 | size, 49 | } 50 | } 51 | if (item.file.size_ape !== 0) { 52 | let size = sizeFormate(item.file.size_ape) 53 | types.push({ type: 'ape', size }) 54 | _types.ape = { 55 | size, 56 | } 57 | } 58 | if (item.file.size_flac !== 0) { 59 | let size = sizeFormate(item.file.size_flac) 60 | types.push({ type: 'flac', size }) 61 | _types.flac = { 62 | size, 63 | } 64 | } 65 | // types.reverse() 66 | return { 67 | singer: this.getSinger(item.singer), 68 | name: item.title, 69 | albumName: item.album.title, 70 | albumId: item.album.mid, 71 | source: 'tx', 72 | interval: formatPlayTime(item.interval), 73 | songId: item.id, 74 | albumMid: item.album.mid, 75 | strMediaMid: item.file.strMediaMid, 76 | songmid: item.mid, 77 | img: (item.album.name === '' || item.album.name === '空') 78 | ? `https://y.gtimg.cn/music/photo_new/T001R500x500M000${item.singer[0].mid}.jpg` 79 | : `https://y.gtimg.cn/music/photo_new/T002R500x500M000${item.album.mid}.jpg`, 80 | lrc: null, 81 | types, 82 | _types, 83 | typeUrl: {}, 84 | } 85 | }) 86 | }, 87 | search(str, page = 1, { limit } = {}) { 88 | if (limit != null) this.limit = limit 89 | // http://newlyric.kuwo.cn/newlyric.lrc?62355680 90 | return this.musicSearch(str, page).then(({ song }) => { 91 | let list = this.handleResult(song.list) 92 | 93 | this.total = song.totalnum 94 | this.page = page 95 | this.allPage = Math.ceil(this.total / this.limit) 96 | 97 | return Promise.resolve({ 98 | list, 99 | allPage: this.allPage, 100 | limit: this.limit, 101 | total: this.total, 102 | source: 'tx', 103 | }) 104 | }) 105 | }, 106 | } 107 | -------------------------------------------------------------------------------- /src/renderer/components/material/FlowBtn.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 65 | 66 | 67 | 123 | -------------------------------------------------------------------------------- /src/main/utils/autoUpdate.js: -------------------------------------------------------------------------------- 1 | const { log } = require('../../common/utils') 2 | const { autoUpdater } = require('electron-updater') 3 | const { mainOn } = require('../../common/ipc') 4 | 5 | autoUpdater.logger = log 6 | // autoUpdater.autoDownload = false 7 | autoUpdater.logger.transports.file.level = 'info' 8 | log.info('App starting...') 9 | 10 | 11 | // ------------------------------------------------------------------- 12 | // Open a window that displays the version 13 | // 14 | // THIS SECTION IS NOT REQUIRED 15 | // 16 | // This isn't required for auto-updates to work, but it's easier 17 | // for the app to show a window than to have to click "About" to see 18 | // that updates are working. 19 | // ------------------------------------------------------------------- 20 | // let win 21 | 22 | function sendStatusToWindow(text) { 23 | log.info(text) 24 | // ipcMain.send('message', text) 25 | } 26 | 27 | 28 | // ------------------------------------------------------------------- 29 | // Auto updates 30 | // 31 | // For details about these events, see the Wiki: 32 | // https://github.com/electron-userland/electron-builder/wiki/Auto-Update#events 33 | // 34 | // The app doesn't need to listen to any events except `update-downloaded` 35 | // 36 | // Uncomment any of the below events to listen for them. Also, 37 | // look in the previous section to see them being used. 38 | // ------------------------------------------------------------------- 39 | // autoUpdater.on('checking-for-update', () => { 40 | // }) 41 | // autoUpdater.on('update-available', (ev, info) => { 42 | // }) 43 | // autoUpdater.on('update-not-available', (ev, info) => { 44 | // }) 45 | // autoUpdater.on('error', (ev, err) => { 46 | // }) 47 | // autoUpdater.on('download-progress', (ev, progressObj) => { 48 | // }) 49 | // autoUpdater.on('update-downloaded', (ev, info) => { 50 | // // Wait 5 seconds, then quit and install 51 | // // In your application, you don't need to wait 5 seconds. 52 | // // You could call autoUpdater.quitAndInstall(); immediately 53 | // // setTimeout(function() { 54 | // // autoUpdater.quitAndInstall() 55 | // // }, 5000) 56 | 57 | // }) 58 | 59 | let waitEvent = [] 60 | const handleSendEvent = action => { 61 | if (global.mainWindow) { 62 | setTimeout(() => { // 延迟发送事件,过早发送可能渲染进程还没启动完成 63 | global.mainWindow.webContents.send(action.type, action.info) 64 | }, 2000) 65 | } else { 66 | waitEvent.push(action) 67 | } 68 | } 69 | 70 | module.exports = isFirstCheckedUpdate => { 71 | if (!isFirstCheckedUpdate) { 72 | if (waitEvent.length) { 73 | waitEvent.forEach((event, index) => { 74 | setTimeout(() => { // 延迟发送事件,过早发送可能渲染进程还没启动完成 75 | global.mainWindow.webContents.send(event.type, event.info) 76 | }, 2000 * (index + 1)) 77 | }) 78 | waitEvent = [] 79 | } 80 | return 81 | } 82 | autoUpdater.on('checking-for-update', () => { 83 | sendStatusToWindow('Checking for update...') 84 | }) 85 | autoUpdater.on('update-available', info => { 86 | sendStatusToWindow('Update available.') 87 | handleSendEvent({ type: 'update-available', info }) 88 | }) 89 | autoUpdater.on('update-not-available', info => { 90 | sendStatusToWindow('Update not available.') 91 | handleSendEvent({ type: 'update-not-available', info }) 92 | }) 93 | autoUpdater.on('error', err => { 94 | sendStatusToWindow('Error in auto-updater.') 95 | handleSendEvent({ type: 'update-error', info: err.message }) 96 | }) 97 | autoUpdater.on('download-progress', progressObj => { 98 | let log_message = 'Download speed: ' + progressObj.bytesPerSecond 99 | log_message = log_message + ' - Downloaded ' + progressObj.percent + '%' 100 | log_message = log_message + ' (' + progressObj.transferred + '/' + progressObj.total + ')' 101 | sendStatusToWindow(log_message) 102 | handleSendEvent({ type: 'update-progress', info: progressObj }) 103 | }) 104 | autoUpdater.on('update-downloaded', info => { 105 | sendStatusToWindow('Update downloaded.') 106 | handleSendEvent({ type: 'update-downloaded', info }) 107 | }) 108 | mainOn('quit-update', () => { 109 | setTimeout(() => { 110 | autoUpdater.quitAndInstall(true, true) 111 | }, 1000) 112 | }) 113 | 114 | autoUpdater.checkForUpdates() 115 | } 116 | 117 | -------------------------------------------------------------------------------- /src/renderer/utils/music/wy/leaderboard.js: -------------------------------------------------------------------------------- 1 | import { httpGet, cancelHttp } from '../../request' 2 | import { formatPlayTime } from '../../index' 3 | 4 | export default { 5 | limit: 300, 6 | list: [ 7 | { 8 | id: 'wybsb', 9 | name: '飙升榜', 10 | bangid: '19723756', 11 | }, 12 | { 13 | id: 'wyrgb', 14 | name: '热歌榜', 15 | bangid: '3778678', 16 | }, 17 | { 18 | id: 'wyxgb', 19 | name: '新歌榜', 20 | bangid: '3779629', 21 | }, 22 | { 23 | id: 'wyycb', 24 | name: '原创榜', 25 | bangid: '2884035', 26 | }, 27 | { 28 | id: 'wygdb', 29 | name: '古典榜', 30 | bangid: '71384707', 31 | }, 32 | { 33 | id: 'wydouyb', 34 | name: '抖音榜', 35 | bangid: '2250011882', 36 | }, 37 | { 38 | id: 'wyhyb', 39 | name: '韩语榜', 40 | bangid: '745956260', 41 | }, 42 | { 43 | id: 'wydianyb', 44 | name: '电音榜', 45 | bangid: '1978921795', 46 | }, 47 | { 48 | id: 'wydjb', 49 | name: '电竞榜', 50 | bangid: '2006508653', 51 | }, 52 | { 53 | id: 'wyktvbb', 54 | name: 'KTV唛榜', 55 | bangid: '21845217', 56 | }, 57 | ], 58 | getUrl(id) { 59 | return `https://music.163.com/discover/toplist?id=${id}` 60 | }, 61 | regExps: { 62 | list: /