diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000..04ab4bf --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,34 @@ +version: "2" # required to adjust maintainability checks +checks: + argument-count: + config: + threshold: 4 + complex-logic: + config: + threshold: 4 + file-lines: + config: + threshold: 1000 + method-complexity: + config: + threshold: 6 + method-count: + config: + threshold: 20 + method-lines: + config: + threshold: 500 + nested-control-flow: + config: + threshold: 4 + return-statements: + config: + threshold: 4 + +engines: + duplication: + enabled: false + config: + languages: + javascript: + mass_threshold: 65 diff --git a/README.md b/README.md index d4faa4e..af8299d 100644 --- a/README.md +++ b/README.md @@ -48,27 +48,27 @@ Download the last version on the [website](https://github.com/trazyn/ieaseMusic/releases/latest) or below. #### Mac(10.9+) -[Download](https://github.com/trazyn/ieaseMusic/releases/download/v1.3.0/ieaseMusic-1.3.0-mac.dmg) the `.dmg` file, Or use `homebrew`: +[Download](https://github.com/trazyn/ieaseMusic/releases/download/v1.3.1/ieaseMusic-1.3.1-mac.dmg) the `.dmg` file, Or use `homebrew`: ``` brew cask install ieasemusic ``` #### Linux -[Download](https://github.com/trazyn/ieaseMusic/releases/download/v1.3.0/ieaseMusic-1.3.0-linux-amd64.deb) the `.deb` file for 'Debian / Ubuntu': +[Download](https://github.com/trazyn/ieaseMusic/releases/download/v1.3.1/ieaseMusic-1.3.1-linux-amd64.deb) the `.deb` file for 'Debian / Ubuntu': ``` -$ sudo dpkg -i ieaseMusic-1.3.0-linux-amd64.deb +$ sudo dpkg -i ieaseMusic-1.3.1-linux-amd64.deb ``` -[Download](https://github.com/trazyn/ieaseMusic/releases/download/v1.3.0/ieaseMusic-1.3.0-linux-x86_64.rpm) the `.rpm` file for 'Centos/RHEL': +[Download](https://github.com/trazyn/ieaseMusic/releases/download/v1.3.1/ieaseMusic-1.3.1-linux-x86_64.rpm) the `.rpm` file for 'Centos/RHEL': ``` -$ sudo yum localinstall ieaseMusic-1.3.0-linux-x86_64.rpm +$ sudo yum localinstall ieaseMusic-1.3.1-linux-x86_64.rpm ``` -[Download](https://github.com/trazyn/ieaseMusic/releases/download/v1.3.0/iease-music-1.3.0-x86_64.AppImage) the `.Appimage` file for other distribution: +[Download](https://github.com/trazyn/ieaseMusic/releases/download/v1.3.1/iease-music-1.3.1-x86_64.AppImage) the `.Appimage` file for other distribution: ``` -$ chmod u+x iease-music-1.3.0-x86_64.AppImage -$ ./iease-music-1.3.0-x86_64.AppImage +$ chmod u+x iease-music-1.3.1-x86_64.AppImage +$ ./iease-music-1.3.1-x86_64.AppImage ``` Archlinux `pacman` install: diff --git a/main.js b/main.js index e998d37..eca9c06 100644 --- a/main.js +++ b/main.js @@ -23,6 +23,10 @@ let tray; let mainWindow; let isOsx = _PLATFORM === 'darwin'; let isLinux = _PLATFORM === 'linux'; + +let showMenuBarOnLinux = false; +let revertTrayIcon = false; + // Shared data to other applocation via a unix socket file let shared = { modes: [], @@ -400,7 +404,7 @@ let dockMenu = [ ]; function updateMenu(playing) { - if (!isOsx) { + if (!isOsx && !showMenuBarOnLinux) { return; } @@ -420,6 +424,13 @@ function updateTray(playing) { : `${__dirname}/src/assets/notplaying.png` ; + if (revertTrayIcon) { + icon = playing + ? `${__dirname}/src/assets/playing-dark-panel.png` + : `${__dirname}/src/assets/notplaying-dark-panel.png` + ; + } + if (!tray) { // Init tray icon tray = new Tray(icon); @@ -479,7 +490,9 @@ const createMainWindow = () => { path.join(__dirname, 'src/assets/dock.png') ); // Disable default menu bar - mainWindow.setMenu(null); + if (!showMenuBarOnLinux) { + mainWindow.setMenu(null); + } } mainWindowState.manage(mainWindow); @@ -586,6 +599,9 @@ const createMainWindow = () => { () => debug('Apply proxy: %s', args.proxy) ); + revertTrayIcon = args.revertTrayIcon; + debug(revertTrayIcon); + if (!args.showTray) { if (tray) { tray.destroy(); @@ -638,6 +654,8 @@ const createMainWindow = () => { updater.installAutoUpdater(() => goodbye()); downloader.createDownloader(); mainWindow.webContents.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/603.3.8 (KHTML, like Gecko) Version/10.1.2 Safari/603.3.8'); + + global.mainWindow = mainWindow; debug('Create main process success π»'); }; @@ -666,6 +684,14 @@ app.on('before-quit', e => { process.exit(0); }); app.on('ready', () => { + storage.get('preferences', (err, data) => { + debug(data); + if (!err && data) { + showMenuBarOnLinux = data.showMenuBarOnLinux; + revertTrayIcon = data.revertTrayIcon; + } + }); + createMainWindow(); storage.get('preferences', (err, data) => { diff --git a/package.json b/package.json index bc6fd47..8d4d327 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "iease-music", - "version": "1.3.0", + "version": "1.3.1", "description": "θΏεΊθ―₯ζ―ζε₯½ηη½ζδΊι³δΉζζΎε¨δΊοΌζ²‘ζδΉδΈοΌε¦ζζθ―·ζι π€", "main": "main.js", "scripts": { @@ -99,7 +99,7 @@ "copy-webpack-plugin": "^4.0.1", "cross-env": "^5.2.0", "css-loader": "^1.0.0", - "electron": "^2.0.6", + "electron": "^2.0.9", "electron-builder": "^20.19.2", "eslint": "^5.2.0", "eslint-config-standard": "^11.0.0", @@ -114,7 +114,7 @@ "express": "^4.15.4", "html-loader": "^0.5.1", "html-webpack-plugin": "^3.2.0", - "style-loader": "^0.21.0", + "style-loader": "^0.23.0", "svg-inline-loader": "^0.8.0", "uglifyjs-webpack-plugin": "^1.1.8", "url-loader": "^1.0.1", @@ -129,11 +129,12 @@ "big-integer": "^1.6.25", "classname": "0.0.0", "cookie-parser": "^1.4.3", - "debug": "^3.1.0", + "crypto": "^1.0.1", + "debug": "^4.0.1", "delegate": "^3.1.3", "electron-json-storage": "^4.1.1", "electron-updater": "^3.0.3", - "electron-window-state": "^4.1.1", + "electron-window-state": "^5.0.1", "han": "0.0.7", "ionicons201": "^1.0.0", "libphonenumber-js": "^0.4.44", diff --git a/server/provider/Kuwo.js b/server/provider/Kuwo.js index c83f848..58e9f4f 100644 --- a/server/provider/Kuwo.js +++ b/server/provider/Kuwo.js @@ -52,7 +52,7 @@ export default async(request, keyword, artists) => { }, }); - if (!response) { + if (!response || response === 'IPDeny') { error(chalk.black.bgRed('π§ Nothing.')); return Promise.reject(Error(404)); } diff --git a/server/provider/MiGu.js b/server/provider/MiGu.js index 2ade43e..88b26a7 100644 --- a/server/provider/MiGu.js +++ b/server/provider/MiGu.js @@ -19,7 +19,12 @@ export default async(request, keyword, artists) => { } }); - if (response.success !== true || response.musics.length === 0) { + if ( + false + || response.success !== true + || !response.musics + || response.musics.length === 0 + ) { error(chalk.black.bgRed('π§ Nothing.')); return Promise.reject(Error(404)); } diff --git a/server/provider/index.js b/server/provider/index.js index 7087a31..64a8071 100644 --- a/server/provider/index.js +++ b/server/provider/index.js @@ -1,5 +1,5 @@ -import storage from 'electron-json-storage'; +import fs from 'fs'; import Netease from './Netease'; import QQ from './QQ'; import MiGu from './MiGu'; @@ -7,13 +7,10 @@ import Kugou from './Kugou'; import Baidu from './Baidu'; import Xiami from './Xiami'; import Kuwo from './Kuwo'; +import storage from '../../common/storage'; async function getPreferences() { - return new Promise(resolve => { - storage.get('preferences', (err, data) => { - resolve(err ? {} : data); - }); - }); + return await storage.get('preferences') || {}; } async function exe(plugins, ...args) { @@ -53,6 +50,23 @@ async function getFlac(keyword, artists) { return exe([QQ], keyword, artists, true); } +async function loadFromLocal(id) { + var downloaded = (await storage.get('downloaded')) || {}; + var task = downloaded[id]; + + if (task) { + if (fs.existsSync(task.path) === false) { + delete downloaded[id]; + await storage.set('downloaded', downloaded); + return; + } + + return { + src: encodeURI(`file://${task.path}`) + }; + } +} + async function getTrack(keyword, artists, id /** This id is only work for netease music */) { var preferences = await getPreferences(); var enginers = preferences.enginers; @@ -97,6 +111,7 @@ async function getTrack(keyword, artists, id /** This id is only work for neteas } export { + loadFromLocal, getFlac, getTrack, }; diff --git a/server/router/artist.js b/server/router/artist.js index a306588..752b3b0 100644 --- a/server/router/artist.js +++ b/server/router/artist.js @@ -1,12 +1,33 @@ import express from 'express'; import axios from 'axios'; +import crypto from 'crypto'; import _debug from 'debug'; const debug = _debug('dev:api'); const error = _debug('dev:error'); const router = express(); +// https://github.com/skyline75489/nmdown/blob/ee0f66448b6e64f8b9bdb2f7451a8d4ff63e14c4/cloudmusic/hasher.py +function id2url(id) { + var key = '3go8&$8*3*3h0k(2)2'; + var keyCodes = Array.from(key).map((e, i) => key.charCodeAt(i)); + var fidCodes = Array.from(id).map((e, i) => id.charCodeAt(i)); + + var hashCodes = []; + + for (let i = 0; i < fidCodes.length; i++) { + let code = (fidCodes[i] ^ keyCodes[i % key.length]) & 0XFF; + hashCodes.push(code); + } + + var string = hashCodes.map((e, i) => String.fromCharCode(hashCodes[i])).join(''); + var md5 = crypto.createHash('md5').update(string).digest(); + var result = Buffer.from(md5).toString('base64').replace(/\//g, '_').replace(/\+/g, '-'); + + return `https://p4.music.126.net/${result}/${id}.jpg?param=y177y177`; +} + async function getArtist(id) { var profile = {}; var songs = []; @@ -43,7 +64,7 @@ async function getArtist(id) { album: { id: al.id.toString(), name: al.name, - cover: `${al.picUrl}?param=y100y100`, + cover: id2url(al.pic_str), link: `/player/1/${al.id}` }, artists: ar.map(e => ({ @@ -84,7 +105,8 @@ async function getAlbums(id) { id: e.id.toString(), name: e.name, cover: e.picUrl, - link: `/player/1/${e.id}` + link: `/player/1/${e.id}`, + publishTime: e.publishTime, })); } } catch (ex) { diff --git a/server/router/player.js b/server/router/player.js index 7cdacdf..6403900 100644 --- a/server/router/player.js +++ b/server/router/player.js @@ -189,7 +189,7 @@ router.get('/song/:id/:name/:artists/:flac?', cache('3 minutes', onlyStatus200), try { // Get the highquality track - song = await selector.getFlac(name, artists, true); + song = await selector.loadFromLocal(id) || await selector.getFlac(name, artists, true); } catch (ex) { error(ex); } @@ -204,6 +204,7 @@ router.get('/song/:id/:name/:artists/:flac?', cache('3 minutes', onlyStatus200), } } } catch (ex) { + error(ex); debug(chalk.red.underline.bold(`π Not found: "${name} - ${artists}"`)); } diff --git a/src/app.js b/src/app.js index f0425b1..499ee50 100644 --- a/src/app.js +++ b/src/app.js @@ -118,6 +118,15 @@ class App extends Component { playing.toggle(true); }); + // Get the playlist + ipcRenderer.on('request-playlist', () => { + let downloader = remote.getGlobal('downloader'); + + if (downloader) { + downloader.webContents.send('response-playlist', JSON.stringify(controller.playlist)); + } + }); + // Right click menu window.addEventListener('contextmenu', e => { let logined = me.hasLogin(); @@ -173,7 +182,7 @@ class App extends Component { { label: 'Download π', click: () => { - ipcRenderer.send('download', { song: JSON.stringify(controller.song) }); + ipcRenderer.send('download', { songs: JSON.stringify(controller.song) }); } }, { diff --git a/src/assets/notplaying-dark-panel.png b/src/assets/notplaying-dark-panel.png new file mode 100644 index 0000000..1ecd9f1 Binary files /dev/null and b/src/assets/notplaying-dark-panel.png differ diff --git a/src/assets/playing-dark-panel.png b/src/assets/playing-dark-panel.png new file mode 100644 index 0000000..d1dbad9 Binary files /dev/null and b/src/assets/playing-dark-panel.png differ diff --git a/src/js/components/Controller/classes.js b/src/js/components/Controller/classes.js index a0f7c39..16e6f77 100644 --- a/src/js/components/Controller/classes.js +++ b/src/js/components/Controller/classes.js @@ -49,6 +49,8 @@ export default theme => { '& $playing:after': { bottom: 16, + opacity: 1, + visibility: 'visible', } }, @@ -75,6 +77,8 @@ export default theme => { right: 0, bottom: 2, display: 'inline-block', + opacity: 0, + visibility: 'hidden', padding: '10px 6px', fontFamily: 'Roboto', fontSize: 12, diff --git a/src/js/components/Preferences/index.js b/src/js/components/Preferences/index.js index a7465e6..66fe4a6 100644 --- a/src/js/components/Preferences/index.js +++ b/src/js/components/Preferences/index.js @@ -56,6 +56,10 @@ class Preferences extends Component { var { showTray, setShowTray, + showMenuBarOnLinux, + setShowMenuBarOnLinux, + revertTrayIcon, + setRevertTrayIcon, alwaysOnTop, setAlwaysOnTop, autoPlay, @@ -111,6 +115,29 @@ class Preferences extends Component { onChange={e => setShowTray(e.target.checked)} /> + + + Show menu bar on Linux + Only work on Linux. Restart needed. + + + setShowMenuBarOnLinux(e.target.checked)} /> + + + + + Revert tray icon to fit dark panel + + + setRevertTrayIcon(e.target.checked)} /> + + Auto play at started diff --git a/src/js/pages/Artist/classes.js b/src/js/pages/Artist/classes.js index 787218c..1b9c56a 100644 --- a/src/js/pages/Artist/classes.js +++ b/src/js/pages/Artist/classes.js @@ -25,7 +25,8 @@ export default theme => ({ }, '& canvas': { - transform: 'translateY(246px) translateX(4px)', + position: 'absolute', + top: -176, zIndex: -1, } }, diff --git a/src/js/stores/me.js b/src/js/stores/me.js index 1480467..f016a9c 100644 --- a/src/js/stores/me.js +++ b/src/js/stores/me.js @@ -114,8 +114,9 @@ class Me { self.profile = getProfile(response.data); done(); - await home.load(); await storage.set('profile', self.profile); + await self.init(); + await home.load(); self.logining = false; return; } diff --git a/src/js/stores/preferences.js b/src/js/stores/preferences.js index 3be7fb1..48f3101 100644 --- a/src/js/stores/preferences.js +++ b/src/js/stores/preferences.js @@ -14,6 +14,8 @@ import lastfm from 'utils/lastfm'; class Preferences { @observable show = false; @observable showTray = false; + @observable showMenuBarOnLinux = false; + @observable revertTrayIcon = false; @observable alwaysOnTop = false; @observable showNotification = true; @observable autoPlay = true; @@ -43,6 +45,8 @@ class Preferences { var preferences = await storage.get('preferences'); var { showTray = self.showTray, + showMenuBarOnLinux = self.showMenuBarOnLinux, + revertTrayIcon = self.revertTrayIcon, alwaysOnTop = self.alwaysOnTop, showNotification = self.showNotification, autoPlay = self.autoPlay, @@ -60,6 +64,8 @@ class Preferences { } = preferences; self.showTray = !!showTray; + self.showMenuBarOnLinux = !!showMenuBarOnLinux; + self.revertTrayIcon = !!revertTrayIcon; self.alwaysOnTop = !!alwaysOnTop; self.showNotification = !!showNotification; self.autoPlay = !!autoPlay; @@ -83,10 +89,12 @@ class Preferences { } @action async save() { - var { showTray, alwaysOnTop, showNotification, autoPlay, naturalScroll, port, volume, highquality, backgrounds, autoupdate, scrobble, lastfm, enginers, proxy, downloads } = self; + var { showTray, showMenuBarOnLinux, revertTrayIcon, alwaysOnTop, showNotification, autoPlay, naturalScroll, port, volume, highquality, backgrounds, autoupdate, scrobble, lastfm, enginers, proxy, downloads } = self; await storage.set('preferences', { showTray, + showMenuBarOnLinux, + revertTrayIcon, alwaysOnTop, showNotification, autoPlay, @@ -108,6 +116,7 @@ class Preferences { showTray, alwaysOnTop, proxy, + revertTrayIcon }); } @@ -116,6 +125,16 @@ class Preferences { self.save(); } + @action setShowMenuBarOnLinux(showMenuBarOnLinux) { + self.showMenuBarOnLinux = showMenuBarOnLinux; + self.save(); + } + + @action setRevertTrayIcon(revertTrayIcon) { + self.revertTrayIcon = revertTrayIcon; + self.save(); + } + @action setAlwaysOnTop(alwaysOnTop) { self.alwaysOnTop = alwaysOnTop; self.save(); diff --git a/src/js/utils/colors.js b/src/js/utils/colors.js index 6b96ff6..585b548 100644 --- a/src/js/utils/colors.js +++ b/src/js/utils/colors.js @@ -7,7 +7,7 @@ const pallet = { mint: '#48cfad', dribbble: '#ea4c89', twitter: '#55acee', - google: '#039be5', + google: '#5090fb', }; const gradients = [ diff --git a/submodules/downloader/index.js b/submodules/downloader/index.js index 3e59ea9..4cfbf2e 100644 --- a/submodules/downloader/index.js +++ b/submodules/downloader/index.js @@ -12,14 +12,53 @@ import _debug from 'debug'; import pkg from '../../package.json'; import storage from '../../common/storage'; +import config from '../../config'; +import helper from '../../src/js/utils/helper'; const KEY = 'downloaded'; -const _DOWNLOAD_DIR = path.join(app.getPath('music'), pkg.name); +const MUSIC_DIR = app.getPath('music') || app.getPath('home'); +const DOWNLOAD_DIR = path.join(MUSIC_DIR, pkg.name); let debug = _debug('dev:submodules:downloader'); let error = _debug('dev:submodules:downloader:error'); let downloader; let cancels = {}; +let queue = createQueue(2); + +function createQueue(max) { + var waitingGroup = []; + var mapping = {}; + var queue = { + waiting(task) { + return new Promise( + (resolve, reject) => { + // Save the resolve callback + mapping[task.id] = () => resolve(); + // Add to queue + waitingGroup.push(task); + + if (waitingGroup.length <= max) { + resolve(); + } + } + ); + }, + done(task) { + mapping[task.id](); + delete mapping[task.id]; + + waitingGroup = waitingGroup.filter(e => e.id !== task.id); + + // Resolve the next task + let next = waitingGroup.pop(); + if (next) { + mapping[next.id](); + } + } + }; + + return queue; +} function isDev() { return process.mainModule.filename.indexOf('app.asar') === -1; @@ -27,7 +66,7 @@ function isDev() { async function getDownloads() { var preferences = await storage.get('preferences'); - var downloads = preferences.downloads || _DOWNLOAD_DIR; + var downloads = preferences.downloads || DOWNLOAD_DIR; // Make sure the download directory already exists if (fs.existsSync(downloads) === false) { @@ -38,6 +77,42 @@ async function getDownloads() { return downloads; } +async function getDownloadLink(song) { + var downloadLink = (song.data || {}).src; + + return new Promise( + (resolve, reject) => { + if (downloadLink) { + resolve(downloadLink); + return; + } + + var url = `/api/player/song/${song.id}/${encodeURIComponent(helper.clearWith(song.name, ['οΌ', '(']))}/${encodeURIComponent(song.artists.map(e => e.name).join(','))}/1`; + + request( + { + url: `http://localhost:${config.api.port}${url}`, + json: true, + timeout: 10000, + }, + (err, response, data) => { + if (err) { + reject(err); + return; + } + + if (!data.song.src) { + reject(new Error('404')); + return; + } + + resolve(data.song.src); + } + ); + } + ); +} + async function syncDownloaded() { try { var downloaded = await storage.get(KEY); @@ -49,12 +124,17 @@ async function syncDownloaded() { Object.keys(downloaded).forEach( e => { var task = downloaded[e]; - debug('Check task: %s:%s', task.id, task.path); + debug('Check task: %s => %s', task.id, task.path); if (!task.id) { throw Error('Invailid storage'); } + // Keep the failed tasks + if (task.success === false) { + return; + } + if ( false || !task.path @@ -80,7 +160,7 @@ async function writeFile(url, filepath, cb, canceler) { if (!state) { callback.size.transferred = callback.size.total; // eslint-disable-next-line - return callback({ percent: 1, size: callback.size }); + return callback({ percent: 1, size: callback.size || 0 }); } var { percent, size } = state; @@ -88,56 +168,57 @@ async function writeFile(url, filepath, cb, canceler) { }; return new Promise((resolve, reject) => { - try { - var r = request({ - url, - headers: { - 'Origin': 'http://music.163.com', - 'Referer': 'http://music.163.com/', + var r = request({ + url, + headers: { + 'Origin': 'http://music.163.com', + 'Referer': 'http://music.163.com/', + }, + timeout: 10000, + }); + + rp(r) + .on('error', + err => { + delete cancels[canceler]; + reject(err); } - }); - - rp(r) - .on('error', - err => { - delete cancels[canceler]; - throw err; - } - ) - .on('progress', - state => { - callback(state); - callback.size = state.size; - } - ) - .on('end', - // WTF? Why no state given?? - // eslint-disable-next-line - () => { - callback(); - resolve(); - - delete cancels[canceler]; - } - ) - .pipe(fs.createWriteStream(filepath)) - ; + ) + .on('progress', + state => { + callback.size = state.size || {}; + callback(state); + } + ) + .on('end', + // WTF? Why no state given?? + // eslint-disable-next-line + () => { + callback(); + resolve(); + + delete cancels[canceler]; + } + ) + .pipe(fs.createWriteStream(filepath)) + ; - if (canceler) { - cancels[canceler] = () => r.abort(); - } - } catch (ex) { - syncTask(); - reject(ex); + if (canceler) { + cancels[canceler] = () => r.abort(); } }); } async function download(task) { + await queue.waiting(task); + + // Mark task as processed + task.waiting = false; + try { var downloads = await getDownloads(); var song = task.payload; - var src = song.data.src; + var src = await getDownloadLink(song); var imagefile = (await tmp.file()).path; var trackfile = path.join( downloads, @@ -145,7 +226,6 @@ async function download(task) { ); task.path = trackfile; - // Tell the render downlaod has started updateTask(task); @@ -182,11 +262,15 @@ async function download(task) { }; let success = nodeID3.write(tags, trackfile); + queue.done(task); + if (!success) { throw Error('Failed to write ID3 tags: \'%s\'', trackfile); } } catch (ex) { error(ex); + queue.done(task); + failTask(task, ex); fs.unlink(trackfile); fs.unlink(imagefile); } @@ -232,9 +316,10 @@ function createDownloader() { // Download track ipcMain.on('download', (event, args) => { - var song = JSON.parse(args.song); + var songs = JSON.parse(args.songs); - addTask(song); + songs = Array.isArray(songs) ? songs : [songs]; + addTasks(songs); } ); @@ -259,6 +344,7 @@ function createDownloader() { ); syncDownloaded(); + global.downloader = downloader; } function removeTasks(tasks) { @@ -283,13 +369,15 @@ function removeTasks(tasks) { } function failTask(task, err) { + task.success = false; downloader.webContents.send( - 'download-failure', + 'download-failed', { task, err } ); } function doneTask(task) { + task.success = true; downloader.webContents.send( 'download-success', { task } @@ -304,26 +392,35 @@ function updateTask(task) { } function syncTask(id) { - syncDownloaded(); downloader.webContents.send('download-sync', { id }); } -function addTask(item) { - debug('Download song: \'%s\'', item.id); - var task = { - id: item.id, - progress: 0, - date: +new Date(), - size: 0, - path: null, - payload: item, - }; +function addTasks(songs) { + var tasks = []; + + songs.map( + e => { + debug('Download song: \'%s\'', e.id); + var task = { + id: e.id, + progress: 0, + date: +new Date(), + size: 0, + path: null, + waiting: true, + payload: e, + }; + + tasks.push(task); + } + ); - download(task); downloader.webContents.send( 'download-begin', - { task } + { tasks } ); + + tasks.map(e => download(e)); } export default { diff --git a/submodules/downloader/viewport/index.js b/submodules/downloader/viewport/index.js index 246d06d..21b9ace 100644 --- a/submodules/downloader/viewport/index.js +++ b/submodules/downloader/viewport/index.js @@ -10,16 +10,20 @@ import 'app/global.css'; import theme from 'config/theme'; import stores from './stores'; import Downloader from './views/Downloader'; +import List from './views/List'; +/* eslint-disable */ render( - + + , document.getElementById('root') ); +/* eslint-enable */ diff --git a/submodules/downloader/viewport/stores.js b/submodules/downloader/viewport/stores.js index 1803db6..db01728 100644 --- a/submodules/downloader/viewport/stores.js +++ b/submodules/downloader/viewport/stores.js @@ -1,65 +1,71 @@ -import { observable, action, autorun } from 'mobx'; +import { observable, action, transaction } from 'mobx'; import storage from 'common/storage'; +import { ipcRenderer, remote } from 'electron'; const KEY = 'downloaded'; class Stores { - @observable tasks = []; - @observable mapping = {}; - - constructor() { - autorun( - () => { - var mapping = this.mapping; - var tasks = []; - - tasks = Object.keys(mapping).map( - (e, index) => { - return mapping[e]; - } - ); - - tasks.sort((a, b) => a.date < b.date); - this.tasks = tasks; - }, - { delay: 500 } - ); - } + @observable tasks = new Map(); + @observable playlist = []; @action.bound load = async() => { try { - var mapping = await storage.get(KEY); - this.mapping = mapping; + var persistence = await storage.get(KEY); + + transaction(() => { + Object.keys(persistence).map( + e => this.tasks.set(e, persistence[e]) + ); + }); } catch (ex) { storage.remove(KEY); - this.mapping = {}; + this.tasks.clear(); } } + save = () => { + var persistence = {}; + var items = Array.from(this.tasks.entries()); + + items.map( + ([key, value]) => { + if (value.progress === 1 || value.success === false) { + persistence[key] = value; + } + } + ); + storage.set(KEY, persistence); + } + + isPersistence = (task) => { + return (this.tasks.get(task.id) || {}).success; + } + @action.bound updateTask = (task) => { - this.mapping[task.id] = task; + // You can not repeat an inprogress task + var exists = this.tasks.get(task.id); + if (exists && task.waiting === true) { + return; + } + this.tasks.set(task.id, task); } @action.bound - doneTask = (task) => { - var mapping = {}; + batchTask = (tasks) => { + transaction(() => { + tasks.map( + task => this.updateTask(task) + ); + }); + } + @action.bound + doneTask = (task) => { this.updateTask(task); - - this.tasks.map( - e => { - if (e.progress === 1) { - mapping[e.id] = e; - } - } - ); - - // Immediate modify the object without delay, then save to storage - mapping[task.id] = task; - storage.set(KEY, mapping); + this.save(); } @action.bound @@ -68,16 +74,31 @@ class Stores { items.forEach( e => { - delete this.mapping[e.id]; + this.tasks.delete(e.id); } ); - storage.set(KEY, this.mapping); + this.save(); } @action.bound - failTask = (item) => { - delete this.mapping[item.id]; - storage.set(KEY, this.mapping); + getPlaylist = () => { + return new Promise( + (resolve, reject) => { + var timer = setTimeout( + () => resolve(false), + 5000 + ); + + ipcRenderer.once('response-playlist', (e, data) => { + clearTimeout(timer); + data = JSON.parse(data); + this.playlist = data.songs; + resolve(true); + }); + + remote.getGlobal('mainWindow').webContents.send('request-playlist'); + } + ); } }; diff --git a/submodules/downloader/viewport/views/Downloader/classes.js b/submodules/downloader/viewport/views/Downloader/classes.js index 968b5e5..477bb8f 100644 --- a/submodules/downloader/viewport/views/Downloader/classes.js +++ b/submodules/downloader/viewport/views/Downloader/classes.js @@ -27,12 +27,6 @@ export default theme => ({ display: 'flex', justifyContent: 'center', alignItems: 'center', - transition: '.2s', - }, - - '& nav > a:hover': { - backgroundColor: colors.pallet.google, - color: 'white', }, '& section': { @@ -41,7 +35,7 @@ export default theme => ({ overflowX: 'hidden', }, - '& aside': { + '& section aside': { position: 'relative', display: 'flex', width: 235, @@ -69,13 +63,20 @@ export default theme => ({ display: 'flex', flexDirection: 'row', alignItems: 'center', + justifyContent: 'space-between', width: '100vw', height: 40, - paddingLeft: 12, backgroundColor: 'white', boxShadow: '0 6px 24px rgba(0, 0, 0, .1)', }, + '& footer aside': { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-around', + }, + '& button': { display: 'flex', flexDirection: 'row', @@ -177,6 +178,37 @@ export default theme => ({ } }, + actions: { + position: 'absolute', + bottom: 0, + right: -13, + height: 64, + width: 100, + display: 'flex', + justifyContent: 'center', + flexDirection: 'column', + alignItems: 'center', + color: '#333', + fontWeight: '500', + fontFamily: 'Roboto', + + '& button': { + cursor: 'pointer', + }, + + '& button:first-child': { + color: colors.pallet.google, + }, + + '& button:last-child': { + color: colors.pallet.grape, + }, + + '& button:hover': { + color: 'inherit', + }, + }, + nothing: { display: 'flex', height: '100%', diff --git a/submodules/downloader/viewport/views/Downloader/index.js b/submodules/downloader/viewport/views/Downloader/index.js index 323d43f..2a5b8cc 100644 --- a/submodules/downloader/viewport/views/Downloader/index.js +++ b/submodules/downloader/viewport/views/Downloader/index.js @@ -2,6 +2,7 @@ import React, { Component } from 'react'; import { inject, observer } from 'mobx-react'; import injectSheet from 'react-jss'; +import { Link } from 'react-router-dom'; import delegate from 'delegate'; import moment from 'moment'; import { ipcRenderer, shell } from 'electron'; @@ -48,7 +49,7 @@ class Downloader extends Component { } componentDidMount() { - var { stores: { load, updateTask, doneTask, failTask } } = this.props; + var { stores: { load, batchTask, updateTask, doneTask } } = this.props; delegate( this.navs, 'a[data-index]', 'click', @@ -58,6 +59,7 @@ class Downloader extends Component { } ); + ipcRenderer.removeAllListeners('download-sync'); ipcRenderer.on('download-sync', (e, args) => { // Reload downloaded items from disk @@ -65,27 +67,42 @@ class Downloader extends Component { } ); + ipcRenderer.removeAllListeners('download-begin'); ipcRenderer.on('download-begin', (e, args) => { - let song = args.task.payload; - let notification = new window.Notification('π Donwload Track', { - icon: song.album.cover, - body: `${song.name} - ${song.artists.map(e => e.name).join(' / ')}`, - }); - - notification.onclick = () => { - ipcRenderer.send('download-show'); - }; - updateTask(args.task); + let songs = args.tasks.map( + e => e.payload + ); + batchTask(args.tasks); + + if (songs.length === 1) { + let song = songs[0]; + + let notification = new window.Notification('π Donwload Track', { + icon: song.album.cover, + body: `${song.name} - ${song.artists.map(e => e.name).join(' / ')}`, + }); + + notification.onclick = () => { + ipcRenderer.send('download-show'); + }; + } else { + // eslint-disable-next-line + new window.Notification('π Donwload Track', { + body: `${songs.length} download tasks in queue~` + }); + } } ); + ipcRenderer.removeAllListeners('download-progress'); ipcRenderer.on('download-progress', (e, args) => { updateTask(args.task); } ); + ipcRenderer.removeAllListeners('download-success'); ipcRenderer.on('download-success', (e, args) => { let song = args.task.payload; @@ -101,7 +118,8 @@ class Downloader extends Component { } ); - ipcRenderer.on('download-failure', + ipcRenderer.removeAllListeners('download-failed'); + ipcRenderer.on('download-failed', (e, args) => { let song = args.task.payload; @@ -110,7 +128,8 @@ class Downloader extends Component { icon: song.album.cover, body: `${song.name} - ${song.artists.map(e => e.name).join(' / ')}`, }); - failTask(args.task, args.err); + doneTask(args.task); + updateTask(args.task); } ); @@ -126,19 +145,21 @@ class Downloader extends Component { var { removeTasks, tasks } = this.props.stores; var confirmed = await this.showConfirm(); + tasks = Array.from(tasks.values()); + if (confirmed) { removeTasks(tasks); ipcRenderer.send('download-remove', { tasks: JSON.stringify(tasks) }); } } - renderDetail(item) { + renderDetail(task) { var { classes, stores: { removeTasks } } = this.props; - var song = item.payload; + var song = task.payload; var name = song.name; var artists = song.artists.map((e, index) => e.name).join(); - if (item.progress === 1) { + if (task.waiting === true) { return ( + ); + } + if (task.progress === 1) { + return ( +
Only work on Linux. Restart needed.