├── dist ├── web │ └── .gitkeep └── electron │ └── .gitkeep ├── static └── .gitkeep ├── src ├── renderer │ ├── assets │ │ ├── .gitkeep │ │ └── logo.png │ ├── components │ │ ├── pages │ │ │ ├── Settings.vue │ │ │ ├── Downloads.vue │ │ │ ├── Auth.vue │ │ │ └── Main.vue │ │ └── Sidebar.vue │ ├── store │ │ ├── index.js │ │ └── modules │ │ │ ├── tracks.js │ │ │ └── shared.js │ ├── main.js │ ├── modules │ │ ├── Track.js │ │ └── icons.js │ ├── router │ │ └── index.js │ └── App.vue ├── main │ ├── config.default.json │ ├── shared.js │ ├── index.dev.js │ ├── store.js │ ├── event-handler.js │ ├── index.js │ ├── api.js │ ├── deezer.js │ ├── dist │ │ └── md5.js │ └── crypto.js └── index.ejs ├── docs └── screen.png ├── .gitignore ├── README.md ├── appveyor.yml ├── .babelrc ├── .travis.yml ├── .electron-vue ├── dev-client.js ├── webpack.main.config.js ├── build.js ├── webpack.web.config.js ├── dev-runner.js └── webpack.renderer.config.js └── package.json /dist/web/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dist/electron/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/renderer/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frannyfx/freezer/HEAD/docs/screen.png -------------------------------------------------------------------------------- /src/renderer/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frannyfx/freezer/HEAD/src/renderer/assets/logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dist/electron/* 3 | dist/web/* 4 | build/* 5 | !build/icons 6 | node_modules/ 7 | npm-debug.log 8 | npm-debug.log.* 9 | thumbs.db 10 | !.gitkeep 11 | .vscode/* 12 | .vscode/ 13 | -------------------------------------------------------------------------------- /src/main/config.default.json: -------------------------------------------------------------------------------- 1 | { 2 | "deezer": { 3 | "loggedIn": false, 4 | "sessionId": null, 5 | "apiToken": null 6 | }, 7 | "downloads": { 8 | "quality": 1, 9 | "directory": null, 10 | "library": [] 11 | } 12 | } -------------------------------------------------------------------------------- /src/main/shared.js: -------------------------------------------------------------------------------- 1 | // Main window 2 | var mainWindow = null; 3 | 4 | function getMainWindow() { 5 | return mainWindow; 6 | } 7 | 8 | function setMainWindow(m) { 9 | mainWindow = m; 10 | } 11 | 12 | export default { 13 | getMainWindow, 14 | setMainWindow 15 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # freezer 2 | Download and decrypt tracks from Deezer in style. 3 | 4 | ![Freezer screeenshot](docs/screen.png) 5 | 6 | This is simply for proof of concept purposes. I do not condone music piracy or listening to Maroon 5. 7 | 8 | ## Known issues 9 | Your access token to Deezer will periodically expire around every 24hrs, hence you'll have to log in again and again every time. -------------------------------------------------------------------------------- /src/renderer/components/pages/Settings.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /src/renderer/components/pages/Downloads.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /src/renderer/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | import { createPersistedState, createSharedMutations } from 'vuex-electron' 5 | 6 | import shared from './modules/shared' 7 | import tracks from './modules/tracks' 8 | 9 | Vue.use(Vuex) 10 | 11 | export default new Vuex.Store({ 12 | modules: { 13 | shared, 14 | tracks 15 | }, 16 | plugins: [ 17 | 18 | ], 19 | strict: process.env.NODE_ENV !== 'production' 20 | }) 21 | -------------------------------------------------------------------------------- /src/renderer/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import axios from 'axios'; 3 | 4 | import App from './App'; 5 | import router from './router'; 6 | import store from './store'; 7 | 8 | import Icons from "./modules/icons"; 9 | 10 | if (!process.env.IS_WEB) 11 | Vue.use(require('vue-electron')); 12 | 13 | Vue.http = Vue.prototype.$http = axios; 14 | Vue.config.productionTip = false; 15 | 16 | /* eslint-disable no-new */ 17 | new Vue({ 18 | components: { App }, 19 | router, 20 | store, 21 | template: '' 22 | }).$mount('#app'); 23 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 0.1.{build} 2 | 3 | branches: 4 | only: 5 | - master 6 | 7 | image: Visual Studio 2017 8 | platform: 9 | - x64 10 | 11 | cache: 12 | - node_modules 13 | - '%APPDATA%\npm-cache' 14 | - '%USERPROFILE%\.electron' 15 | - '%USERPROFILE%\AppData\Local\Yarn\cache' 16 | 17 | init: 18 | - git config --global core.autocrlf input 19 | 20 | install: 21 | - ps: Install-Product node 8 x64 22 | - git reset --hard HEAD 23 | - yarn 24 | - node --version 25 | 26 | build_script: 27 | - yarn build 28 | 29 | test: off 30 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "comments": false, 3 | "env": { 4 | "main": { 5 | "presets": [ 6 | ["env", { 7 | "targets": { "node": 7 } 8 | }], 9 | "stage-0" 10 | ] 11 | }, 12 | "renderer": { 13 | "presets": [ 14 | ["env", { 15 | "modules": false 16 | }], 17 | "stage-0" 18 | ] 19 | }, 20 | "web": { 21 | "presets": [ 22 | ["env", { 23 | "modules": false 24 | }], 25 | "stage-0" 26 | ] 27 | } 28 | }, 29 | "plugins": ["transform-runtime"] 30 | } 31 | -------------------------------------------------------------------------------- /src/renderer/modules/Track.js: -------------------------------------------------------------------------------- 1 | class Track { 2 | constructor(track, download) { 3 | this.id = track.id; 4 | this.data = track; 5 | this.progress = 0; 6 | this.status = !download ? "Idle" : "Done"; 7 | this.downloaded = !!download; 8 | this.downloadPath = !!download ? download.path : null; 9 | } 10 | 11 | updateProgress(progress) { 12 | this.progress = progress.progress; 13 | this.status = progress.status; 14 | 15 | if (this.progress == 1) { 16 | this.downloaded = true; 17 | } 18 | } 19 | 20 | download() { 21 | this.progress = 0; 22 | this.status = "Downloading"; 23 | } 24 | } 25 | 26 | export default Track; -------------------------------------------------------------------------------- /src/renderer/store/modules/tracks.js: -------------------------------------------------------------------------------- 1 | const state = { 2 | search: { 3 | results: {}, 4 | resultIds: [] 5 | } 6 | } 7 | 8 | const mutations = { 9 | addTrack(state, track) { 10 | state.search.results[track.id] = track; 11 | if (state.search.resultIds.indexOf(track.id) == -1) 12 | state.search.resultIds.push(track.id); 13 | }, 14 | updateProgress(state, progress) { 15 | state.search.results[progress.id].updateProgress(progress); 16 | } 17 | } 18 | 19 | const actions = { 20 | addTrack(context, track) { 21 | context.commit("addTrack", track); 22 | }, 23 | updateProgress(context, progress) { 24 | context.commit("updateProgress", progress); 25 | } 26 | } 27 | 28 | export default { 29 | namespaced: true, 30 | state, 31 | mutations, 32 | actions 33 | } 34 | -------------------------------------------------------------------------------- /src/renderer/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | Vue.use(Router) 5 | 6 | export default new Router({ 7 | routes: [ 8 | { 9 | path: "/", 10 | name: "main", 11 | component: require("@/components/pages/Main").default 12 | }, 13 | { 14 | path: "/auth", 15 | name: "auth", 16 | component: require("@/components/pages/Auth").default 17 | }, 18 | { 19 | path: "/downloads", 20 | name: "downloads", 21 | component: require("@/components/pages/Downloads").default 22 | }, 23 | { 24 | path: "/settings", 25 | name: "settings", 26 | component: require("@/components/pages/Settings").default 27 | }, 28 | { 29 | path: '*', 30 | redirect: '/' 31 | } 32 | ] 33 | }) 34 | -------------------------------------------------------------------------------- /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 | 8 | /* eslint-disable */ 9 | 10 | // Install `electron-debug` with `devtron` 11 | require('electron-debug')({ showDevTools: false }) 12 | 13 | // Install `vue-devtools` 14 | require('electron').app.on('ready', () => { 15 | let installExtension = require('electron-devtools-installer') 16 | installExtension.default(installExtension.VUEJS_DEVTOOLS) 17 | .then(() => {}) 18 | .catch(err => { 19 | console.log('Unable to install `vue-devtools`: \n', err) 20 | }) 21 | }) 22 | 23 | // Require `main` process to boot app 24 | require('./index') -------------------------------------------------------------------------------- /src/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | freebooter 6 | <% if (htmlWebpackPlugin.options.nodeModules) { %> 7 | 8 | 11 | <% } %> 12 | 13 | 14 |
15 | 16 | <% if (!require("process").browser) { %> 17 | 20 | <% } %> 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/renderer/components/pages/Auth.vue: -------------------------------------------------------------------------------- 1 | 7 | 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | osx_image: xcode8.3 2 | sudo: required 3 | dist: trusty 4 | language: c 5 | matrix: 6 | include: 7 | - os: osx 8 | - os: linux 9 | env: CC=clang CXX=clang++ npm_config_clang=1 10 | compiler: clang 11 | cache: 12 | directories: 13 | - node_modules 14 | - "$HOME/.electron" 15 | - "$HOME/.cache" 16 | addons: 17 | apt: 18 | packages: 19 | - libgnome-keyring-dev 20 | - icnsutils 21 | before_install: 22 | - mkdir -p /tmp/git-lfs && curl -L https://github.com/github/git-lfs/releases/download/v1.2.1/git-lfs-$([ 23 | "$TRAVIS_OS_NAME" == "linux" ] && echo "linux" || echo "darwin")-amd64-1.2.1.tar.gz 24 | | tar -xz -C /tmp/git-lfs --strip-components 1 && /tmp/git-lfs/git-lfs pull 25 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install --no-install-recommends -y icnsutils graphicsmagick xz-utils; fi 26 | install: 27 | - nvm install 10 28 | - curl -o- -L https://yarnpkg.com/install.sh | bash 29 | - source ~/.bashrc 30 | - npm install -g xvfb-maybe 31 | - yarn 32 | script: 33 | - yarn run build 34 | branches: 35 | only: 36 | - master 37 | -------------------------------------------------------------------------------- /src/renderer/store/modules/shared.js: -------------------------------------------------------------------------------- 1 | const state = { 2 | deezer: { 3 | loggedIn: false, 4 | sessionId: "", 5 | apiToken: "" 6 | }, 7 | downloads: { 8 | quality: 1, 9 | directory: null, 10 | library: {}, 11 | libraryIds: [] 12 | } 13 | } 14 | 15 | const mutations = { 16 | setLoginData(state, deezer) { 17 | state.deezer.loggedIn = deezer.loggedIn; 18 | state.deezer.sessionId = deezer.sessionId; 19 | state.deezer.apiToken = deezer.apiToken; 20 | }, 21 | setDownloads(state, downloads) { 22 | state.downloads.quality = downloads.quality; 23 | state.downloads.directory = downloads.directory; 24 | 25 | // Handle library 26 | state.downloads.library = {}; 27 | downloads.library.map(download => state.downloads.library[download.track.id] = download); 28 | state.downloads.libraryIds = downloads.library.map(download => download.track.id); 29 | } 30 | } 31 | 32 | const actions = { 33 | setData(context, newData) { 34 | if (newData.deezer != undefined) { 35 | console.log(newData.downloads.library); 36 | context.commit("setLoginData", newData.deezer); 37 | context.commit("setDownloads", newData.downloads); 38 | } 39 | } 40 | } 41 | 42 | export default { 43 | namespaced: true, 44 | state, 45 | mutations, 46 | actions 47 | } 48 | -------------------------------------------------------------------------------- /.electron-vue/dev-client.js: -------------------------------------------------------------------------------- 1 | const hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 2 | 3 | hotClient.subscribe(event => { 4 | /** 5 | * Reload browser when HTMLWebpackPlugin emits a new index.html 6 | * 7 | * Currently disabled until jantimon/html-webpack-plugin#680 is resolved. 8 | * https://github.com/SimulatedGREG/electron-vue/issues/437 9 | * https://github.com/jantimon/html-webpack-plugin/issues/680 10 | */ 11 | // if (event.action === 'reload') { 12 | // window.location.reload() 13 | // } 14 | 15 | /** 16 | * Notify `mainWindow` when `main` process is compiling, 17 | * giving notice for an expected reload of the `electron` process 18 | */ 19 | if (event.action === 'compiling') { 20 | document.body.innerHTML += ` 21 | 34 | 35 |
36 | Compiling Main Process... 37 |
38 | ` 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /src/renderer/modules/icons.js: -------------------------------------------------------------------------------- 1 | // Import icons (tree shaking) 2 | import { library } from "@fortawesome/fontawesome-svg-core"; 3 | 4 | // Wanted icons 5 | import { faChevronRight } from "@fortawesome/free-solid-svg-icons/faChevronRight"; 6 | import { faChevronLeft} from "@fortawesome/free-solid-svg-icons/faChevronLeft"; 7 | import { faCheck } from "@fortawesome/free-solid-svg-icons/faCheck"; 8 | import { faTimes } from "@fortawesome/free-solid-svg-icons/faTimes"; 9 | import { faExclamationTriangle } from "@fortawesome/free-solid-svg-icons/faExclamationTriangle"; 10 | import { faInfoCircle } from "@fortawesome/free-solid-svg-icons/faInfoCircle"; 11 | import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons/faExternalLinkAlt"; 12 | import { faHome } from "@fortawesome/free-solid-svg-icons/faHome"; 13 | import { faPlus } from "@fortawesome/free-solid-svg-icons/faPlus"; 14 | import { faSearch } from "@fortawesome/free-solid-svg-icons/faSearch"; 15 | import { faDownload } from "@fortawesome/free-solid-svg-icons/faDownload"; 16 | import { faCog } from "@fortawesome/free-solid-svg-icons/faCog"; 17 | import { faSyncAlt } from "@fortawesome/free-solid-svg-icons/faSyncAlt"; 18 | import { faHistory } from "@fortawesome/free-solid-svg-icons/faHistory"; 19 | 20 | library.add( 21 | faChevronRight, 22 | faCheck, 23 | faTimes, 24 | faExclamationTriangle, 25 | faInfoCircle, 26 | faExternalLinkAlt, 27 | faChevronLeft, 28 | faHome, 29 | faPlus, 30 | faSearch, 31 | faDownload, 32 | faCog, 33 | faSyncAlt, 34 | faHistory 35 | ); -------------------------------------------------------------------------------- /.electron-vue/webpack.main.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.BABEL_ENV = 'main' 4 | 5 | const path = require('path') 6 | const { dependencies } = require('../package.json') 7 | const webpack = require('webpack') 8 | 9 | const MinifyPlugin = require("babel-minify-webpack-plugin") 10 | 11 | let mainConfig = { 12 | entry: { 13 | main: path.join(__dirname, '../src/main/index.js') 14 | }, 15 | externals: [ 16 | ...Object.keys(dependencies || {}) 17 | ], 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.js$/, 22 | use: 'babel-loader', 23 | exclude: /node_modules/ 24 | }, 25 | { 26 | test: /\.node$/, 27 | use: 'node-loader' 28 | } 29 | ] 30 | }, 31 | node: { 32 | __dirname: process.env.NODE_ENV !== 'production', 33 | __filename: process.env.NODE_ENV !== 'production' 34 | }, 35 | output: { 36 | filename: '[name].js', 37 | libraryTarget: 'commonjs2', 38 | path: path.join(__dirname, '../dist/electron') 39 | }, 40 | plugins: [ 41 | new webpack.NoEmitOnErrorsPlugin() 42 | ], 43 | resolve: { 44 | extensions: ['.js', '.json', '.node'] 45 | }, 46 | target: 'electron-main' 47 | } 48 | 49 | /** 50 | * Adjust mainConfig for development settings 51 | */ 52 | if (process.env.NODE_ENV !== 'production') { 53 | mainConfig.plugins.push( 54 | new webpack.DefinePlugin({ 55 | '__static': `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"` 56 | }) 57 | ) 58 | } 59 | 60 | /** 61 | * Adjust mainConfig for production settings 62 | */ 63 | if (process.env.NODE_ENV === 'production') { 64 | mainConfig.plugins.push( 65 | new MinifyPlugin(), 66 | new webpack.DefinePlugin({ 67 | 'process.env.NODE_ENV': '"production"' 68 | }) 69 | ) 70 | } 71 | 72 | module.exports = mainConfig 73 | -------------------------------------------------------------------------------- /src/main/store.js: -------------------------------------------------------------------------------- 1 | import { app, remote, ipcMain } from "electron"; 2 | import path from "path"; 3 | import { promises as fs } from "fs"; 4 | import Defaults from "./config.default.json"; 5 | import Shared from "./shared"; 6 | 7 | class Store { 8 | constructor() { 9 | let dataPath = (app || remote.app).getPath("appData"); 10 | this.path = path.join(dataPath, "freezer-settings.json"); 11 | this.data = {}; 12 | } 13 | 14 | get(key) { 15 | try { 16 | let domains = key.split("."); 17 | var current = this.data; 18 | for (var i = 0; i < domains.length; i++) { 19 | current = current[domains[i]]; 20 | } 21 | 22 | return current; 23 | } catch (e) { 24 | return null; 25 | } 26 | } 27 | 28 | async push(key, value) { 29 | let current = this.get(key); 30 | if (current == null || !Array.isArray(current)) 31 | return false; 32 | 33 | current.push(value); 34 | await this.write(); 35 | } 36 | 37 | async set(key, val) { 38 | let domains = key.split("."); 39 | var current = this.data; 40 | try { 41 | for (var i = 0; i < domains.length; i++) { 42 | if (current[domains[i]] == undefined) 43 | current[domains[i]] = {}; 44 | 45 | // Last domain 46 | if (i == domains.length - 1) { 47 | current[domains[i]] = val; 48 | } 49 | 50 | current = current[domains[i]]; 51 | } 52 | } catch (e) { 53 | console.warn("Unable to write property."); 54 | } 55 | 56 | await this.write(); 57 | } 58 | 59 | async load() { 60 | try { 61 | this.data = JSON.parse(await fs.readFile(this.path)); 62 | } catch (e) { 63 | this.data = Defaults; 64 | } 65 | } 66 | 67 | async write() { 68 | try { 69 | // Tell renderer data is updated. 70 | let mainWindow = Shared.getMainWindow(); 71 | if (mainWindow) mainWindow.webContents.send("data", this.data); 72 | 73 | // Write to disk. 74 | await fs.writeFile(this.path, JSON.stringify(this.data, null, 4)); 75 | } catch (e) { 76 | console.error("Unable to write config file.", this.path, e); 77 | } 78 | } 79 | } 80 | 81 | // Instance 82 | let store = new Store(); 83 | 84 | // Export singleton instance 85 | export default store; -------------------------------------------------------------------------------- /src/renderer/components/Sidebar.vue: -------------------------------------------------------------------------------- 1 | 8 | 32 | -------------------------------------------------------------------------------- /src/main/event-handler.js: -------------------------------------------------------------------------------- 1 | import { ipcMain, BrowserWindow } from "electron"; 2 | import Store from "./store"; 3 | import API from "./api"; 4 | import path from "path"; 5 | import Deezer from "./deezer"; 6 | 7 | function initialise() { 8 | ipcMain.on("want-data", async (event, data) => { 9 | event.sender.send("data", Store.data); 10 | }); 11 | 12 | ipcMain.on("autocomplete", async (event, data) => { 13 | let result = await API.sendAutocompleteRequest(data.query); 14 | event.sender.send("autocomplete-response", result); 15 | }); 16 | 17 | ipcMain.on("login", async (event, data) => { 18 | console.log("User requested to log in."); 19 | 20 | let logInWindow = new BrowserWindow({ 21 | width: 500, 22 | height: 500, 23 | webPreferences: { 24 | nodeIntegration: true, 25 | preload: path.resolve(path.join(__dirname, "../../dist/electron/inject", "login-inject.js")) 26 | } 27 | }); 28 | 29 | logInWindow.webContents.session.clearStorageData([]); 30 | logInWindow.loadURL("https://www.deezer.com/en/login"); 31 | logInWindow.webContents.on("did-navigate", async (event, url) => { 32 | if (url.indexOf("login") == -1) { 33 | try { 34 | // Get the cookie. 35 | let cookies = await logInWindow.webContents.session.cookies.get({ 36 | url: "https://www.deezer.com", 37 | name: "sid" 38 | }); 39 | 40 | // Ignore invalid cookies. 41 | if (cookies.length == 0 || cookies[0].name != "sid") { 42 | console.log("Unable to get SID."); 43 | return; 44 | } 45 | 46 | // Test cookie and get API token. 47 | console.log("Testing cookie..."); 48 | Store.set("deezer.sessionId", cookies[0].value); 49 | if (await API.testConnection()) { 50 | console.log("Success!"); 51 | logInWindow.close(); 52 | return; 53 | } 54 | } catch (e) { 55 | console.log("Failed to get cookies.", e); 56 | } 57 | } 58 | }); 59 | }); 60 | 61 | ipcMain.on("search", async (event, data) => { 62 | let result = await API.sendSearchRequest(data.query); 63 | event.sender.send("search-response", result); 64 | }); 65 | 66 | ipcMain.on("download", async (event, data) => { 67 | console.log(`Downloading track ${data.track.id}...`); 68 | Deezer.downloadAndDecrypt(data.track, data.quality ? data.quality : Store.get("downloads.quality")); 69 | 70 | }); 71 | } 72 | 73 | export default { 74 | initialise 75 | }; -------------------------------------------------------------------------------- /src/main/index.js: -------------------------------------------------------------------------------- 1 | // Imports 2 | import { app, BrowserWindow, ipcMain } from 'electron'; 3 | import Store from "./store"; 4 | import API from "./api"; 5 | import Shared from "./shared"; 6 | 7 | // Modules 8 | import EventHandler from "./event-handler"; 9 | import shared from './shared'; 10 | 11 | /** 12 | * Set `__static` path to static files in production 13 | * https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-static-assets.html 14 | */ 15 | if (process.env.NODE_ENV !== 'development') { 16 | global.__static = require('path').join(__dirname, '/static').replace(/\\/g, '\\\\'); 17 | } 18 | 19 | let mainWindow; 20 | const winURL = process.env.NODE_ENV === 'development' ? `http://localhost:9080` : `file://${__dirname}/index.html`; 21 | 22 | function createWindow () { 23 | // Create main window and load URL. 24 | mainWindow = new BrowserWindow({ 25 | title: "Freezer", 26 | titleBarStyle: "hiddenInset", 27 | width: 700, 28 | height: 450, 29 | useContentSize: true, 30 | resizable: false, 31 | webPreferences: { 32 | nodeIntegration: true 33 | } 34 | }); 35 | 36 | mainWindow.loadURL(winURL); 37 | 38 | // Set the main window. 39 | shared.setMainWindow(mainWindow); 40 | 41 | // Handle close. 42 | mainWindow.on('closed', () => { 43 | mainWindow = null 44 | }); 45 | } 46 | 47 | app.on('ready', async () => { 48 | // Load settings 49 | await Store.load(); 50 | 51 | // Initialise API 52 | await API.testConnection(); 53 | 54 | // Create the window. 55 | createWindow(); 56 | 57 | // Initialise event handler. 58 | EventHandler.initialise(); 59 | }); 60 | 61 | app.on('window-all-closed', () => { 62 | if (process.platform !== 'darwin') { 63 | app.quit(); 64 | } 65 | }); 66 | 67 | app.on('activate', () => { 68 | if (mainWindow === null) { 69 | createWindow(); 70 | } 71 | }); 72 | 73 | 74 | 75 | /** 76 | * Auto Updater 77 | * 78 | * Uncomment the following code below and install `electron-updater` to 79 | * support auto updating. Code Signing with a valid certificate is required. 80 | * https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-electron-builder.html#auto-updating 81 | */ 82 | 83 | /* 84 | import { autoUpdater } from 'electron-updater' 85 | 86 | autoUpdater.on('update-downloaded', () => { 87 | autoUpdater.quitAndInstall() 88 | }) 89 | 90 | app.on('ready', () => { 91 | if (process.env.NODE_ENV === 'production') autoUpdater.checkForUpdates() 92 | }) 93 | */ 94 | -------------------------------------------------------------------------------- /src/renderer/App.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 38 | 39 | 57 | 58 | 101 | -------------------------------------------------------------------------------- /.electron-vue/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.NODE_ENV = 'production' 4 | 5 | const { say } = require('cfonts') 6 | const chalk = require('chalk') 7 | const del = require('del') 8 | const { spawn } = require('child_process') 9 | const webpack = require('webpack') 10 | const Multispinner = require('multispinner') 11 | 12 | 13 | const mainConfig = require('./webpack.main.config') 14 | const rendererConfig = require('./webpack.renderer.config') 15 | const webConfig = require('./webpack.web.config') 16 | 17 | const doneLog = chalk.bgGreen.white(' DONE ') + ' ' 18 | const errorLog = chalk.bgRed.white(' ERROR ') + ' ' 19 | const okayLog = chalk.bgBlue.white(' OKAY ') + ' ' 20 | const isCI = process.env.CI || false 21 | 22 | if (process.env.BUILD_TARGET === 'clean') clean() 23 | else if (process.env.BUILD_TARGET === 'web') web() 24 | else build() 25 | 26 | function clean () { 27 | del.sync(['build/*', '!build/icons', '!build/icons/icon.*']) 28 | console.log(`\n${doneLog}\n`) 29 | process.exit() 30 | } 31 | 32 | function build () { 33 | greeting() 34 | 35 | del.sync(['dist/electron/*', '!.gitkeep']) 36 | 37 | const tasks = ['main', 'renderer'] 38 | const m = new Multispinner(tasks, { 39 | preText: 'building', 40 | postText: 'process' 41 | }) 42 | 43 | let results = '' 44 | 45 | m.on('success', () => { 46 | process.stdout.write('\x1B[2J\x1B[0f') 47 | console.log(`\n\n${results}`) 48 | console.log(`${okayLog}take it away ${chalk.yellow('`electron-builder`')}\n`) 49 | process.exit() 50 | }) 51 | 52 | pack(mainConfig).then(result => { 53 | results += result + '\n\n' 54 | m.success('main') 55 | }).catch(err => { 56 | m.error('main') 57 | console.log(`\n ${errorLog}failed to build main process`) 58 | console.error(`\n${err}\n`) 59 | process.exit(1) 60 | }) 61 | 62 | pack(rendererConfig).then(result => { 63 | results += result + '\n\n' 64 | m.success('renderer') 65 | }).catch(err => { 66 | m.error('renderer') 67 | console.log(`\n ${errorLog}failed to build renderer process`) 68 | console.error(`\n${err}\n`) 69 | process.exit(1) 70 | }) 71 | } 72 | 73 | function pack (config) { 74 | return new Promise((resolve, reject) => { 75 | config.mode = 'production' 76 | webpack(config, (err, stats) => { 77 | if (err) reject(err.stack || err) 78 | else if (stats.hasErrors()) { 79 | let err = '' 80 | 81 | stats.toString({ 82 | chunks: false, 83 | colors: true 84 | }) 85 | .split(/\r?\n/) 86 | .forEach(line => { 87 | err += ` ${line}\n` 88 | }) 89 | 90 | reject(err) 91 | } else { 92 | resolve(stats.toString({ 93 | chunks: false, 94 | colors: true 95 | })) 96 | } 97 | }) 98 | }) 99 | } 100 | 101 | function web () { 102 | del.sync(['dist/web/*', '!.gitkeep']) 103 | webConfig.mode = 'production' 104 | webpack(webConfig, (err, stats) => { 105 | if (err || stats.hasErrors()) console.log(err) 106 | 107 | console.log(stats.toString({ 108 | chunks: false, 109 | colors: true 110 | })) 111 | 112 | process.exit() 113 | }) 114 | } 115 | 116 | function greeting () { 117 | const cols = process.stdout.columns 118 | let text = '' 119 | 120 | if (cols > 85) text = 'lets-build' 121 | else if (cols > 60) text = 'lets-|build' 122 | else text = false 123 | 124 | if (text && !isCI) { 125 | say(text, { 126 | colors: ['yellow'], 127 | font: 'simple3d', 128 | space: false 129 | }) 130 | } else console.log(chalk.yellow.bold('\n lets-build')) 131 | console.log() 132 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "freezer", 3 | "version": "0.0.1", 4 | "author": "frannyfx", 5 | "description": "Download and decrypt songs from Deezer", 6 | "license": null, 7 | "main": "./dist/electron/main.js", 8 | "scripts": { 9 | "build": "node .electron-vue/build.js && electron-builder ", 10 | "build:darwin": "cross-env BUILD_TARGET=darwin node .electron-vue/build.js", 11 | "build:dir": "node .electron-vue/build.js && electron-builder --dir", 12 | "build:clean": "cross-env BUILD_TARGET=clean node .electron-vue/build.js", 13 | "build:web": "cross-env BUILD_TARGET=web node .electron-vue/build.js", 14 | "dev": "node .electron-vue/dev-runner.js", 15 | "pack": "npm run pack:main && npm run pack:renderer", 16 | "pack:main": "cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.main.config.js", 17 | "pack:renderer": "cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.renderer.config.js", 18 | "postinstall": "" 19 | }, 20 | "build": { 21 | "productName": "Freezer", 22 | "appId": "com.frannyfx.freezer", 23 | "directories": { 24 | "output": "build" 25 | }, 26 | "files": [ 27 | "dist/electron/**/*" 28 | ], 29 | "dmg": { 30 | "contents": [ 31 | { 32 | "x": 410, 33 | "y": 150, 34 | "type": "link", 35 | "path": "/Applications" 36 | }, 37 | { 38 | "x": 130, 39 | "y": 150, 40 | "type": "file" 41 | } 42 | ] 43 | }, 44 | "mac": { 45 | "icon": "build/icons/icon.icns" 46 | }, 47 | "win": { 48 | "icon": "build/icons/icon.ico" 49 | }, 50 | "linux": { 51 | "icon": "build/icons" 52 | } 53 | }, 54 | "dependencies": { 55 | "@fortawesome/fontawesome-svg-core": "^1.2.28", 56 | "@fortawesome/free-solid-svg-icons": "^5.13.0", 57 | "@fortawesome/vue-fontawesome": "^0.1.9", 58 | "aes-js": "^3.1.2", 59 | "axios": "^0.18.1", 60 | "node-id3": "^0.1.16", 61 | "uuid": "^8.1.0", 62 | "vue": "^2.5.16", 63 | "vue-electron": "^1.0.6", 64 | "vue-router": "^3.0.1", 65 | "vuex": "^3.0.1", 66 | "vuex-electron": "^1.0.0" 67 | }, 68 | "devDependencies": { 69 | "ajv": "^6.5.0", 70 | "babel-core": "^6.26.3", 71 | "babel-loader": "^7.1.4", 72 | "babel-minify-webpack-plugin": "^0.3.1", 73 | "babel-plugin-transform-runtime": "^6.23.0", 74 | "babel-preset-env": "^1.7.0", 75 | "babel-preset-stage-0": "^6.24.1", 76 | "babel-register": "^6.26.0", 77 | "cfonts": "^2.1.2", 78 | "chalk": "^2.4.1", 79 | "copy-webpack-plugin": "^4.5.1", 80 | "cross-env": "^5.1.6", 81 | "css-loader": "^0.28.11", 82 | "del": "^3.0.0", 83 | "devtron": "^1.4.0", 84 | "electron": "^9.0.0", 85 | "electron-builder": "^20.19.2", 86 | "electron-debug": "^1.5.0", 87 | "electron-devtools-installer": "^2.2.4", 88 | "electron-packager": "^14.2.1", 89 | "electron-rebuild": "^1.11.0", 90 | "file-loader": "^1.1.11", 91 | "html-webpack-plugin": "^3.2.0", 92 | "mini-css-extract-plugin": "0.4.0", 93 | "multispinner": "^0.2.1", 94 | "node-loader": "^0.6.0", 95 | "node-sass": "^4.9.2", 96 | "sass-loader": "^7.0.3", 97 | "style-loader": "^0.21.0", 98 | "url-loader": "^1.0.1", 99 | "vue-html-loader": "^1.2.4", 100 | "vue-loader": "^15.2.4", 101 | "vue-style-loader": "^4.1.0", 102 | "vue-template-compiler": "^2.5.16", 103 | "webpack": "^4.15.1", 104 | "webpack-cli": "^3.0.8", 105 | "webpack-dev-server": "^3.1.4", 106 | "webpack-hot-middleware": "^2.22.2", 107 | "webpack-merge": "^4.1.3" 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /.electron-vue/webpack.web.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.BABEL_ENV = 'web' 4 | 5 | const path = require('path') 6 | const webpack = require('webpack') 7 | 8 | const MinifyPlugin = require("babel-minify-webpack-plugin") 9 | const CopyWebpackPlugin = require('copy-webpack-plugin') 10 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 11 | const HtmlWebpackPlugin = require('html-webpack-plugin') 12 | const { VueLoaderPlugin } = require('vue-loader') 13 | 14 | let webConfig = { 15 | devtool: '#cheap-module-eval-source-map', 16 | entry: { 17 | web: path.join(__dirname, '../src/renderer/main.js') 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.scss$/, 23 | use: ['vue-style-loader', 'css-loader', 'sass-loader'] 24 | }, 25 | { 26 | test: /\.sass$/, 27 | use: ['vue-style-loader', 'css-loader', 'sass-loader?indentedSyntax'] 28 | }, 29 | { 30 | test: /\.less$/, 31 | use: ['vue-style-loader', 'css-loader', 'less-loader'] 32 | }, 33 | { 34 | test: /\.css$/, 35 | use: ['vue-style-loader', 'css-loader'] 36 | }, 37 | { 38 | test: /\.html$/, 39 | use: 'vue-html-loader' 40 | }, 41 | { 42 | test: /\.js$/, 43 | use: 'babel-loader', 44 | include: [ path.resolve(__dirname, '../src/renderer') ], 45 | exclude: /node_modules/ 46 | }, 47 | { 48 | test: /\.vue$/, 49 | use: { 50 | loader: 'vue-loader', 51 | options: { 52 | extractCSS: true, 53 | loaders: { 54 | sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1', 55 | scss: 'vue-style-loader!css-loader!sass-loader', 56 | less: 'vue-style-loader!css-loader!less-loader' 57 | } 58 | } 59 | } 60 | }, 61 | { 62 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 63 | use: { 64 | loader: 'url-loader', 65 | query: { 66 | limit: 10000, 67 | name: 'imgs/[name].[ext]' 68 | } 69 | } 70 | }, 71 | { 72 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 73 | use: { 74 | loader: 'url-loader', 75 | query: { 76 | limit: 10000, 77 | name: 'fonts/[name].[ext]' 78 | } 79 | } 80 | } 81 | ] 82 | }, 83 | plugins: [ 84 | new VueLoaderPlugin(), 85 | new MiniCssExtractPlugin({filename: 'styles.css'}), 86 | new HtmlWebpackPlugin({ 87 | filename: 'index.html', 88 | template: path.resolve(__dirname, '../src/index.ejs'), 89 | minify: { 90 | collapseWhitespace: true, 91 | removeAttributeQuotes: true, 92 | removeComments: true 93 | }, 94 | nodeModules: false 95 | }), 96 | new webpack.DefinePlugin({ 97 | 'process.env.IS_WEB': 'true' 98 | }), 99 | new webpack.HotModuleReplacementPlugin(), 100 | new webpack.NoEmitOnErrorsPlugin() 101 | ], 102 | output: { 103 | filename: '[name].js', 104 | path: path.join(__dirname, '../dist/web') 105 | }, 106 | resolve: { 107 | alias: { 108 | '@': path.join(__dirname, '../src/renderer'), 109 | 'vue$': 'vue/dist/vue.esm.js' 110 | }, 111 | extensions: ['.js', '.vue', '.json', '.css'] 112 | }, 113 | target: 'web' 114 | } 115 | 116 | /** 117 | * Adjust webConfig for production settings 118 | */ 119 | if (process.env.NODE_ENV === 'production') { 120 | webConfig.devtool = '' 121 | 122 | webConfig.plugins.push( 123 | new MinifyPlugin(), 124 | new CopyWebpackPlugin([ 125 | { 126 | from: path.join(__dirname, '../static'), 127 | to: path.join(__dirname, '../dist/web/static'), 128 | ignore: ['.*'] 129 | } 130 | ]), 131 | new webpack.DefinePlugin({ 132 | 'process.env.NODE_ENV': '"production"' 133 | }), 134 | new webpack.LoaderOptionsPlugin({ 135 | minimize: true 136 | }) 137 | ) 138 | } 139 | 140 | module.exports = webConfig 141 | -------------------------------------------------------------------------------- /src/main/api.js: -------------------------------------------------------------------------------- 1 | import Axios from "axios"; 2 | import Store from "./store"; 3 | import https from "https"; 4 | 5 | async function sendRequest(action = "GET", method, body) { 6 | // Get Deezer variables 7 | let apiToken = Store.get("deezer.apiToken"); 8 | let sessionId = Store.get("deezer.sessionId"); 9 | 10 | try { 11 | // Send request 12 | let response = await Axios.request({ 13 | url: `https://www.deezer.com/ajax/gw-light.php?method=${method}&input=3&api_version=1.0&api_token=${method != "deezer.getUserData" ? apiToken : ""}`, 14 | headers: { 15 | Cookie: `sid=${sessionId};` 16 | }, 17 | method: action, 18 | data: body, 19 | httpsAgent: new https.Agent({ 20 | rejectUnauthorized: false 21 | }) 22 | }); 23 | 24 | return response; 25 | } catch (e) { 26 | console.log(e); 27 | console.warn("Unable to send Deezer request.", action, method, body); 28 | return null; 29 | } 30 | } 31 | 32 | async function testConnection() { 33 | if (Store.get("deezer.sessionId") == null) 34 | return false; 35 | 36 | let response = await sendRequest("GET", "deezer.getUserData"); 37 | try { 38 | // Not logged in if UID = 0 39 | if (response.data.results["USER"]["USER_ID"] == 0) { 40 | throw new Error("Not logged in."); 41 | } 42 | 43 | 44 | let apiToken = response.data.results.checkForm; 45 | await Store.set("deezer.apiToken", apiToken); 46 | await Store.set("deezer.loggedIn", true); 47 | return true; 48 | } catch (e) { 49 | await Store.set("deezer.apiToken", ""); 50 | await Store.set("deezer.sessionId", ""); 51 | await Store.set("deezer.loggedIn", false); 52 | return false; 53 | } 54 | } 55 | 56 | async function sendAutocompleteRequest(query) { 57 | let response = await sendRequest("POST", "search_getSuggestedQueries", { query }); 58 | 59 | // Parse response 60 | if (response.data.error.length != 0) 61 | return []; 62 | 63 | // Define an item converter 64 | let itemConverter = function (item, isHistory) { 65 | let converted = { 66 | query: item["QUERY"], 67 | isHistory 68 | }; 69 | 70 | if (!isHistory) { 71 | converted.id = item["ITEM_ID"]; 72 | converted.countryRank = item["COUNTRY_RANK"]; 73 | converted.globalRank = item["GLOBAL_RANK"]; 74 | converted.itemType = item["ITEM_TYPE"]; 75 | } 76 | 77 | return converted; 78 | } 79 | 80 | // Convert the items 81 | let items = []; 82 | items.push(...response.data.results["HISTORY"].map(item => itemConverter(item, true))); 83 | items.push(...response.data.results["SUGGESTION"].map(item => itemConverter(item, false))); 84 | return items; 85 | } 86 | 87 | async function sendSearchRequest(query) { 88 | let response = await sendRequest("POST", "search.music", { 89 | query: query, 90 | filter: "ALL", 91 | output: "TRACK", 92 | start: 0, 93 | nb: 30 94 | }); 95 | 96 | // Define an item converter 97 | let results = response.data.results.data.map((item) => { 98 | let converted = { 99 | id: item["SNG_ID"], 100 | title: `${item["SNG_TITLE"]} ${item["VERSION"] != undefined ? item["VERSION"] : ""}`.trim(), 101 | type: item["TYPE"], 102 | md5: item["MD5_ORIGIN"], 103 | version: item["MEDIA_VERSION"], 104 | token: { 105 | token: item["TRACK_TOKEN"], 106 | expiry: item["TRACK_TOKEN_EXPIRE"] 107 | }, 108 | gain: item["GAIN"], 109 | filesizes: { 110 | aac_64: item["FILESIZE_AAC_64"], 111 | mp3_64: item["FILESIZE_MP3_64"], 112 | mp3_128: item["FILESIZE_MP3_128"], 113 | mp3_256: item["FILESIZE_MP3_256"], 114 | mp3_320: item["FILESIZE_MP3_320"], 115 | flac: item["FILESIZE_FLAC"] 116 | }, 117 | album: { 118 | id: item["ALB_ID"], 119 | picture: item["ALB_PICTURE"], 120 | title: item["ALB_TITLE"], 121 | 122 | }, 123 | artist: { 124 | id: item["ART_ID"], 125 | name: item["ART_NAME"], 126 | picture: item["ART_PICTURE"] 127 | }, 128 | artists: [], 129 | duration: item["DURATION"], 130 | rank: item["RANK_SNG"], 131 | }; 132 | 133 | converted.artists = item["ARTISTS"].map((artist) => { 134 | return { 135 | id: artist["ART_ID"], 136 | order: artist["ARTISTS_SONGS_ORDER"], 137 | name: artist["ART_NAME"], 138 | picture: artist["ART_PICTURE"], 139 | rank: artist["RANK"] 140 | }; 141 | }); 142 | 143 | return converted; 144 | }); 145 | 146 | return results; 147 | } 148 | 149 | export default { 150 | sendAutocompleteRequest, 151 | sendSearchRequest, 152 | testConnection 153 | } -------------------------------------------------------------------------------- /.electron-vue/dev-runner.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const chalk = require('chalk') 4 | const electron = require('electron') 5 | const path = require('path') 6 | const { say } = require('cfonts') 7 | const { spawn } = require('child_process') 8 | const webpack = require('webpack') 9 | const WebpackDevServer = require('webpack-dev-server') 10 | const webpackHotMiddleware = require('webpack-hot-middleware') 11 | 12 | const mainConfig = require('./webpack.main.config') 13 | const rendererConfig = require('./webpack.renderer.config') 14 | 15 | let electronProcess = null 16 | let manualRestart = false 17 | let hotMiddleware 18 | 19 | function logStats (proc, data) { 20 | let log = '' 21 | 22 | log += chalk.yellow.bold(`┏ ${proc} Process ${new Array((19 - proc.length) + 1).join('-')}`) 23 | log += '\n\n' 24 | 25 | if (typeof data === 'object') { 26 | data.toString({ 27 | colors: true, 28 | chunks: false 29 | }).split(/\r?\n/).forEach(line => { 30 | log += ' ' + line + '\n' 31 | }) 32 | } else { 33 | log += ` ${data}\n` 34 | } 35 | 36 | log += '\n' + chalk.yellow.bold(`┗ ${new Array(28 + 1).join('-')}`) + '\n' 37 | 38 | console.log(log) 39 | } 40 | 41 | function startRenderer () { 42 | return new Promise((resolve, reject) => { 43 | rendererConfig.entry.renderer = [path.join(__dirname, 'dev-client')].concat(rendererConfig.entry.renderer) 44 | rendererConfig.mode = 'development' 45 | const compiler = webpack(rendererConfig) 46 | hotMiddleware = webpackHotMiddleware(compiler, { 47 | log: false, 48 | heartbeat: 2500 49 | }) 50 | 51 | compiler.hooks.compilation.tap('compilation', compilation => { 52 | compilation.hooks.htmlWebpackPluginAfterEmit.tapAsync('html-webpack-plugin-after-emit', (data, cb) => { 53 | hotMiddleware.publish({ action: 'reload' }) 54 | cb() 55 | }) 56 | }) 57 | 58 | compiler.hooks.done.tap('done', stats => { 59 | logStats('Renderer', stats) 60 | }) 61 | 62 | const server = new WebpackDevServer( 63 | compiler, 64 | { 65 | contentBase: path.join(__dirname, '../'), 66 | quiet: true, 67 | before (app, ctx) { 68 | app.use(hotMiddleware) 69 | ctx.middleware.waitUntilValid(() => { 70 | resolve() 71 | }) 72 | } 73 | } 74 | ) 75 | 76 | server.listen(9080) 77 | }) 78 | } 79 | 80 | function startMain () { 81 | return new Promise((resolve, reject) => { 82 | mainConfig.entry.main = [path.join(__dirname, '../src/main/index.dev.js')].concat(mainConfig.entry.main) 83 | mainConfig.mode = 'development' 84 | const compiler = webpack(mainConfig) 85 | 86 | compiler.hooks.watchRun.tapAsync('watch-run', (compilation, done) => { 87 | logStats('Main', chalk.white.bold('compiling...')) 88 | hotMiddleware.publish({ action: 'compiling' }) 89 | done() 90 | }) 91 | 92 | compiler.watch({}, (err, stats) => { 93 | if (err) { 94 | console.log(err) 95 | return 96 | } 97 | 98 | logStats('Main', stats) 99 | 100 | if (electronProcess && electronProcess.kill) { 101 | manualRestart = true 102 | process.kill(electronProcess.pid) 103 | electronProcess = null 104 | startElectron() 105 | 106 | setTimeout(() => { 107 | manualRestart = false 108 | }, 5000) 109 | } 110 | 111 | resolve() 112 | }) 113 | }) 114 | } 115 | 116 | function startElectron () { 117 | var args = [ 118 | '--inspect=5858', 119 | path.join(__dirname, '../dist/electron/main.js') 120 | ] 121 | 122 | // detect yarn or npm and process commandline args accordingly 123 | if (process.env.npm_execpath.endsWith('yarn.js')) { 124 | args = args.concat(process.argv.slice(3)) 125 | } else if (process.env.npm_execpath.endsWith('npm-cli.js')) { 126 | args = args.concat(process.argv.slice(2)) 127 | } 128 | 129 | electronProcess = spawn(electron, args) 130 | 131 | electronProcess.stdout.on('data', data => { 132 | electronLog(data, 'blue') 133 | }) 134 | electronProcess.stderr.on('data', data => { 135 | electronLog(data, 'red') 136 | }) 137 | 138 | electronProcess.on('close', () => { 139 | if (!manualRestart) process.exit() 140 | }) 141 | } 142 | 143 | function electronLog (data, color) { 144 | let log = '' 145 | data = data.toString().split(/\r?\n/) 146 | data.forEach(line => { 147 | log += ` ${line}\n` 148 | }) 149 | if (/[0-9A-z]+/.test(log)) { 150 | console.log( 151 | chalk[color].bold('┏ Electron -------------------') + 152 | '\n\n' + 153 | log + 154 | chalk[color].bold('┗ ----------------------------') + 155 | '\n' 156 | ) 157 | } 158 | } 159 | 160 | function greeting () { 161 | const cols = process.stdout.columns 162 | let text = '' 163 | 164 | if (cols > 104) text = 'electron-vue' 165 | else if (cols > 76) text = 'electron-|vue' 166 | else text = false 167 | 168 | if (text) { 169 | say(text, { 170 | colors: ['yellow'], 171 | font: 'simple3d', 172 | space: false 173 | }) 174 | } else console.log(chalk.yellow.bold('\n electron-vue')) 175 | console.log(chalk.blue(' getting ready...') + '\n') 176 | } 177 | 178 | function init () { 179 | greeting() 180 | 181 | Promise.all([startRenderer(), startMain()]) 182 | .then(() => { 183 | startElectron() 184 | }) 185 | .catch(err => { 186 | console.error(err) 187 | }) 188 | } 189 | 190 | init() 191 | -------------------------------------------------------------------------------- /.electron-vue/webpack.renderer.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.BABEL_ENV = 'renderer' 4 | 5 | const path = require('path') 6 | const { dependencies } = require('../package.json') 7 | const webpack = require('webpack') 8 | 9 | const MinifyPlugin = require("babel-minify-webpack-plugin") 10 | const CopyWebpackPlugin = require('copy-webpack-plugin') 11 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 12 | const HtmlWebpackPlugin = require('html-webpack-plugin') 13 | const { VueLoaderPlugin } = require('vue-loader') 14 | 15 | /** 16 | * List of node_modules to include in webpack bundle 17 | * 18 | * Required for specific packages like Vue UI libraries 19 | * that provide pure *.vue files that need compiling 20 | * https://simulatedgreg.gitbooks.io/electron-vue/content/en/webpack-configurations.html#white-listing-externals 21 | */ 22 | let whiteListedModules = ['vue'] 23 | 24 | let rendererConfig = { 25 | devtool: '#cheap-module-eval-source-map', 26 | entry: { 27 | renderer: path.join(__dirname, '../src/renderer/main.js') 28 | }, 29 | externals: [ 30 | ...Object.keys(dependencies || {}).filter(d => !whiteListedModules.includes(d)) 31 | ], 32 | module: { 33 | rules: [ 34 | { 35 | test: /\.scss$/, 36 | use: ['vue-style-loader', 'css-loader', 'sass-loader'] 37 | }, 38 | { 39 | test: /\.sass$/, 40 | use: ['vue-style-loader', 'css-loader', 'sass-loader?indentedSyntax'] 41 | }, 42 | { 43 | test: /\.less$/, 44 | use: ['vue-style-loader', 'css-loader', 'less-loader'] 45 | }, 46 | { 47 | test: /\.css$/, 48 | use: ['vue-style-loader', 'css-loader'] 49 | }, 50 | { 51 | test: /\.html$/, 52 | use: 'vue-html-loader' 53 | }, 54 | { 55 | test: /\.js$/, 56 | use: 'babel-loader', 57 | exclude: /node_modules/ 58 | }, 59 | { 60 | test: /\.node$/, 61 | use: 'node-loader' 62 | }, 63 | { 64 | test: /\.vue$/, 65 | use: { 66 | loader: 'vue-loader', 67 | options: { 68 | extractCSS: process.env.NODE_ENV === 'production', 69 | loaders: { 70 | sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1', 71 | scss: 'vue-style-loader!css-loader!sass-loader', 72 | less: 'vue-style-loader!css-loader!less-loader' 73 | } 74 | } 75 | } 76 | }, 77 | { 78 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 79 | use: { 80 | loader: 'url-loader', 81 | query: { 82 | limit: 10000, 83 | name: 'imgs/[name]--[folder].[ext]' 84 | } 85 | } 86 | }, 87 | { 88 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 89 | loader: 'url-loader', 90 | options: { 91 | limit: 10000, 92 | name: 'media/[name]--[folder].[ext]' 93 | } 94 | }, 95 | { 96 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 97 | use: { 98 | loader: 'url-loader', 99 | query: { 100 | limit: 10000, 101 | name: 'fonts/[name]--[folder].[ext]' 102 | } 103 | } 104 | } 105 | ] 106 | }, 107 | node: { 108 | __dirname: process.env.NODE_ENV !== 'production', 109 | __filename: process.env.NODE_ENV !== 'production' 110 | }, 111 | plugins: [ 112 | new VueLoaderPlugin(), 113 | new MiniCssExtractPlugin({filename: 'styles.css'}), 114 | new HtmlWebpackPlugin({ 115 | filename: 'index.html', 116 | template: path.resolve(__dirname, '../src/index.ejs'), 117 | minify: { 118 | collapseWhitespace: true, 119 | removeAttributeQuotes: true, 120 | removeComments: true 121 | }, 122 | nodeModules: process.env.NODE_ENV !== 'production' 123 | ? path.resolve(__dirname, '../node_modules') 124 | : false 125 | }), 126 | new webpack.HotModuleReplacementPlugin(), 127 | new webpack.NoEmitOnErrorsPlugin() 128 | ], 129 | output: { 130 | filename: '[name].js', 131 | libraryTarget: 'commonjs2', 132 | path: path.join(__dirname, '../dist/electron') 133 | }, 134 | resolve: { 135 | alias: { 136 | '@': path.join(__dirname, '../src/renderer'), 137 | 'vue$': 'vue/dist/vue.esm.js' 138 | }, 139 | extensions: ['.js', '.vue', '.json', '.css', '.node'] 140 | }, 141 | target: 'electron-renderer' 142 | } 143 | 144 | /** 145 | * Adjust rendererConfig for development settings 146 | */ 147 | if (process.env.NODE_ENV !== 'production') { 148 | rendererConfig.plugins.push( 149 | new webpack.DefinePlugin({ 150 | '__static': `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"` 151 | }) 152 | ) 153 | } 154 | 155 | /** 156 | * Adjust rendererConfig for production settings 157 | */ 158 | if (process.env.NODE_ENV === 'production') { 159 | rendererConfig.devtool = '' 160 | 161 | rendererConfig.plugins.push( 162 | new MinifyPlugin(), 163 | new CopyWebpackPlugin([ 164 | { 165 | from: path.join(__dirname, '../static'), 166 | to: path.join(__dirname, '../dist/electron/static'), 167 | ignore: ['.*'] 168 | } 169 | ]), 170 | new webpack.DefinePlugin({ 171 | 'process.env.NODE_ENV': '"production"' 172 | }), 173 | new webpack.LoaderOptionsPlugin({ 174 | minimize: true 175 | }) 176 | ) 177 | } 178 | 179 | module.exports = rendererConfig 180 | -------------------------------------------------------------------------------- /src/main/deezer.js: -------------------------------------------------------------------------------- 1 | // Imports 2 | import { app, remote } from "electron"; 3 | import Crypto from "./crypto"; 4 | import Axios from "axios"; 5 | import Store from "./store"; 6 | import fs from "fs"; 7 | import path from "path"; 8 | import Shared from "./shared"; 9 | import {v4 as uuid} from "uuid"; 10 | import NodeID3 from "node-id3"; 11 | 12 | // Enums 13 | const MediaQuality = { 14 | MP3_128: 1, 15 | MP3_320: 3, 16 | FLAC: 9 17 | }; 18 | 19 | // Consts 20 | const CDN = "https://e-cdns-proxy-{0}.dzcdn.net/mobile/1/"; 21 | const downloadPercentage = .5; 22 | const decryptionPercentage = .2; 23 | const artworkPercentage = .2; 24 | const progressUpdateThreshold = 50; 25 | 26 | // Downloads in progress 27 | let progressUpdateThrottle = {}; 28 | 29 | // Decryptions in progerss 30 | let decryptionsInProgress = []; 31 | 32 | function sendDownloadProgress(id, progress, status, force = false) { 33 | // Don't send excessive updates 34 | if (Date.now() - progressUpdateThrottle[id] < progressUpdateThreshold && progress != 1 && !force) 35 | return; 36 | 37 | // Send update 38 | progressUpdateThrottle[id] = Date.now(); 39 | Shared.getMainWindow().webContents.send("download-progress", { 40 | id: id, 41 | progress: progress, 42 | status: status 43 | }); 44 | } 45 | 46 | function getArtwork(track) { 47 | return new Promise(async (resolve, reject) => { 48 | // Get artwork 49 | let parentPath = (app || remote.app).getPath("temp"); 50 | let artworkPath = path.join(parentPath, `${track.id}_${uuid()}.jpg`); 51 | 52 | // Download with stream 53 | try { 54 | let {data, headers} = await Axios({ 55 | url: `https://e-cdns-images.dzcdn.net/images/cover/${track.album.picture}/500x500-000000-80-0-0.jpg`, 56 | method: "GET", 57 | responseType: "stream" 58 | }); 59 | 60 | var downloadedBytes = 0; 61 | const totalBytes = headers["content-length"]; 62 | const writer = fs.createWriteStream(artworkPath); 63 | data.pipe(writer); 64 | 65 | data.on("data", (chunk) => { 66 | // Increment progress 67 | downloadedBytes += chunk.length; 68 | sendDownloadProgress(track.id, downloadPercentage + decryptionPercentage + (downloadedBytes / totalBytes) * artworkPercentage, "Downloading artwork"); 69 | }); 70 | 71 | data.on("close", () => { 72 | sendDownloadProgress(track.id, downloadPercentage + decryptionPercentage + artworkPercentage, "Finalising", true); 73 | resolve(artworkPath); 74 | }); 75 | } catch (e) { 76 | return reject(e); 77 | } 78 | }) 79 | } 80 | 81 | function embedArtwork(track, artworkPath, outputPath) { 82 | return new Promise(resolve => { 83 | let tags = { 84 | title: track.title, 85 | artist: track.artists.reduce((prev, cur, i) => i != track.artists.length - 1 ? prev + cur.name + ", " : prev + cur.name, ""), 86 | album: track.album.title, 87 | APIC: artworkPath 88 | }; 89 | 90 | return resolve(NodeID3.write(tags, outputPath)); 91 | }); 92 | } 93 | 94 | async function decryptionPromise(track, filePath, decryptedPath, outputPath) { 95 | return new Promise(resolve => { 96 | console.log(`Decrypting track ${track.id}...`); 97 | Crypto.initialise(track.id); 98 | Crypto.decryptTrack(filePath, decryptedPath, async (progress) => { 99 | sendDownloadProgress(track.id, downloadPercentage + (decryptionPercentage * progress), "Decrypting"); 100 | 101 | // Finished decrypting, delete temp file. 102 | if (progress == 1) { 103 | resolve(); 104 | await fs.promises.unlink(filePath); 105 | 106 | // Get the artwork 107 | try { 108 | console.log(`Retrieving artwork for track ${track.id}...`); 109 | let artworkPath = await getArtwork(track); 110 | if (await embedArtwork(track, artworkPath, decryptedPath)) { 111 | await fs.promises.rename(decryptedPath, outputPath); 112 | sendDownloadProgress(track.id, 1, "Done"); 113 | console.log(`Download for track ${track.id} complete.`); 114 | await fs.promises.unlink(artworkPath); 115 | } 116 | } catch (e) { 117 | console.log("Failed to include album art.", e); 118 | } 119 | 120 | delete progressUpdateThrottle[track.id]; 121 | } 122 | }); 123 | }); 124 | } 125 | 126 | async function downloadAndDecrypt(track, quality) { 127 | try { 128 | // Get URL 129 | let url = Crypto.getStreamUrl(track.id, CDN, MediaQuality.MP3_128, track.md5, track.version); 130 | 131 | // Get extension 132 | let extension = quality == MediaQuality.FLAC ? "flac" : "mp3"; 133 | 134 | // Get temp folder and a path to save the file 135 | let parentPath = (app || remote.app).getPath("temp"); 136 | let filePath = path.join(parentPath, `${track.id}_${uuid()}.${extension}`); 137 | let decryptedPath = path.join(parentPath, `${track.id}_${uuid()}_decrypted.${extension}`); 138 | 139 | // Get output folder 140 | let downloadsPath = (app || remote.app).getPath("downloads"); 141 | let outputPath = path.join(downloadsPath, `${track.artist.name} - ${track.title} (freezer_${Date.now()}).${extension}`.replace("/", "-")); 142 | 143 | // Start downloading file 144 | const { data, headers } = await Axios({ 145 | url, 146 | method: "GET", 147 | responseType: "stream" 148 | }); 149 | 150 | // Get track length 151 | var downloadedBytes = 0; 152 | const totalBytes = headers["content-length"]; 153 | 154 | // Throttle download progress 155 | progressUpdateThrottle[track.id] = 0; 156 | 157 | // Write to stream 158 | const writer = fs.createWriteStream(filePath); 159 | data.pipe(writer); 160 | 161 | // Update progress 162 | data.on("data", (chunk) => { 163 | // Increment progress 164 | downloadedBytes += chunk.length; 165 | sendDownloadProgress(track.id, (downloadedBytes / totalBytes) * downloadPercentage, "Downloading"); 166 | }); 167 | 168 | data.on("close", async () => { 169 | sendDownloadProgress(track.id, (downloadedBytes / totalBytes) * downloadPercentage, "Waiting for crypto"); 170 | 171 | // Wait for other decryptions to finish 172 | await Promise.all(decryptionsInProgress); 173 | 174 | // Create decryption and push immediately 175 | let decryption = decryptionPromise(track, filePath, decryptedPath, outputPath); 176 | decryptionsInProgress.push(decryption); 177 | 178 | // Wait for decryption to finish, then remove it 179 | await decryption; 180 | decryptionsInProgress.splice(decryptionsInProgress.indexOf(decryption), 1); 181 | 182 | // Store in library 183 | await Store.push("downloads.library", { 184 | track: track, 185 | path: outputPath 186 | }); 187 | }); 188 | } catch (e) { 189 | console.log("Something went wrong while downloading & decrypting the track.", e); 190 | sendDownloadProgress(track.id, 1, "Error"); 191 | } 192 | } 193 | 194 | export default { 195 | MediaQuality, 196 | downloadAndDecrypt 197 | } -------------------------------------------------------------------------------- /src/main/dist/md5.js: -------------------------------------------------------------------------------- 1 | Object.defineProperty(exports, "__esModule", { value: true }); 2 | var Md5 = /** @class */ (function () { 3 | function Md5() { 4 | this._state = new Int32Array(4); 5 | this._buffer = new ArrayBuffer(68); 6 | this._buffer8 = new Uint8Array(this._buffer, 0, 68); 7 | this._buffer32 = new Uint32Array(this._buffer, 0, 17); 8 | this.start(); 9 | } 10 | Md5.hashStr = function (str, raw) { 11 | if (raw === void 0) { raw = false; } 12 | return this.onePassHasher 13 | .start() 14 | .appendStr(str) 15 | .end(raw); 16 | }; 17 | Md5.hashAsciiStr = function (str, raw) { 18 | if (raw === void 0) { raw = false; } 19 | return this.onePassHasher 20 | .start() 21 | .appendAsciiStr(str) 22 | .end(raw); 23 | }; 24 | Md5._hex = function (x) { 25 | var hc = Md5.hexChars; 26 | var ho = Md5.hexOut; 27 | var n; 28 | var offset; 29 | var j; 30 | var i; 31 | for (i = 0; i < 4; i += 1) { 32 | offset = i * 8; 33 | n = x[i]; 34 | for (j = 0; j < 8; j += 2) { 35 | ho[offset + 1 + j] = hc.charAt(n & 0x0F); 36 | n >>>= 4; 37 | ho[offset + 0 + j] = hc.charAt(n & 0x0F); 38 | n >>>= 4; 39 | } 40 | } 41 | return ho.join(''); 42 | }; 43 | Md5._md5cycle = function (x, k) { 44 | var a = x[0]; 45 | var b = x[1]; 46 | var c = x[2]; 47 | var d = x[3]; 48 | // ff() 49 | a += (b & c | ~b & d) + k[0] - 680876936 | 0; 50 | a = (a << 7 | a >>> 25) + b | 0; 51 | d += (a & b | ~a & c) + k[1] - 389564586 | 0; 52 | d = (d << 12 | d >>> 20) + a | 0; 53 | c += (d & a | ~d & b) + k[2] + 606105819 | 0; 54 | c = (c << 17 | c >>> 15) + d | 0; 55 | b += (c & d | ~c & a) + k[3] - 1044525330 | 0; 56 | b = (b << 22 | b >>> 10) + c | 0; 57 | a += (b & c | ~b & d) + k[4] - 176418897 | 0; 58 | a = (a << 7 | a >>> 25) + b | 0; 59 | d += (a & b | ~a & c) + k[5] + 1200080426 | 0; 60 | d = (d << 12 | d >>> 20) + a | 0; 61 | c += (d & a | ~d & b) + k[6] - 1473231341 | 0; 62 | c = (c << 17 | c >>> 15) + d | 0; 63 | b += (c & d | ~c & a) + k[7] - 45705983 | 0; 64 | b = (b << 22 | b >>> 10) + c | 0; 65 | a += (b & c | ~b & d) + k[8] + 1770035416 | 0; 66 | a = (a << 7 | a >>> 25) + b | 0; 67 | d += (a & b | ~a & c) + k[9] - 1958414417 | 0; 68 | d = (d << 12 | d >>> 20) + a | 0; 69 | c += (d & a | ~d & b) + k[10] - 42063 | 0; 70 | c = (c << 17 | c >>> 15) + d | 0; 71 | b += (c & d | ~c & a) + k[11] - 1990404162 | 0; 72 | b = (b << 22 | b >>> 10) + c | 0; 73 | a += (b & c | ~b & d) + k[12] + 1804603682 | 0; 74 | a = (a << 7 | a >>> 25) + b | 0; 75 | d += (a & b | ~a & c) + k[13] - 40341101 | 0; 76 | d = (d << 12 | d >>> 20) + a | 0; 77 | c += (d & a | ~d & b) + k[14] - 1502002290 | 0; 78 | c = (c << 17 | c >>> 15) + d | 0; 79 | b += (c & d | ~c & a) + k[15] + 1236535329 | 0; 80 | b = (b << 22 | b >>> 10) + c | 0; 81 | // gg() 82 | a += (b & d | c & ~d) + k[1] - 165796510 | 0; 83 | a = (a << 5 | a >>> 27) + b | 0; 84 | d += (a & c | b & ~c) + k[6] - 1069501632 | 0; 85 | d = (d << 9 | d >>> 23) + a | 0; 86 | c += (d & b | a & ~b) + k[11] + 643717713 | 0; 87 | c = (c << 14 | c >>> 18) + d | 0; 88 | b += (c & a | d & ~a) + k[0] - 373897302 | 0; 89 | b = (b << 20 | b >>> 12) + c | 0; 90 | a += (b & d | c & ~d) + k[5] - 701558691 | 0; 91 | a = (a << 5 | a >>> 27) + b | 0; 92 | d += (a & c | b & ~c) + k[10] + 38016083 | 0; 93 | d = (d << 9 | d >>> 23) + a | 0; 94 | c += (d & b | a & ~b) + k[15] - 660478335 | 0; 95 | c = (c << 14 | c >>> 18) + d | 0; 96 | b += (c & a | d & ~a) + k[4] - 405537848 | 0; 97 | b = (b << 20 | b >>> 12) + c | 0; 98 | a += (b & d | c & ~d) + k[9] + 568446438 | 0; 99 | a = (a << 5 | a >>> 27) + b | 0; 100 | d += (a & c | b & ~c) + k[14] - 1019803690 | 0; 101 | d = (d << 9 | d >>> 23) + a | 0; 102 | c += (d & b | a & ~b) + k[3] - 187363961 | 0; 103 | c = (c << 14 | c >>> 18) + d | 0; 104 | b += (c & a | d & ~a) + k[8] + 1163531501 | 0; 105 | b = (b << 20 | b >>> 12) + c | 0; 106 | a += (b & d | c & ~d) + k[13] - 1444681467 | 0; 107 | a = (a << 5 | a >>> 27) + b | 0; 108 | d += (a & c | b & ~c) + k[2] - 51403784 | 0; 109 | d = (d << 9 | d >>> 23) + a | 0; 110 | c += (d & b | a & ~b) + k[7] + 1735328473 | 0; 111 | c = (c << 14 | c >>> 18) + d | 0; 112 | b += (c & a | d & ~a) + k[12] - 1926607734 | 0; 113 | b = (b << 20 | b >>> 12) + c | 0; 114 | // hh() 115 | a += (b ^ c ^ d) + k[5] - 378558 | 0; 116 | a = (a << 4 | a >>> 28) + b | 0; 117 | d += (a ^ b ^ c) + k[8] - 2022574463 | 0; 118 | d = (d << 11 | d >>> 21) + a | 0; 119 | c += (d ^ a ^ b) + k[11] + 1839030562 | 0; 120 | c = (c << 16 | c >>> 16) + d | 0; 121 | b += (c ^ d ^ a) + k[14] - 35309556 | 0; 122 | b = (b << 23 | b >>> 9) + c | 0; 123 | a += (b ^ c ^ d) + k[1] - 1530992060 | 0; 124 | a = (a << 4 | a >>> 28) + b | 0; 125 | d += (a ^ b ^ c) + k[4] + 1272893353 | 0; 126 | d = (d << 11 | d >>> 21) + a | 0; 127 | c += (d ^ a ^ b) + k[7] - 155497632 | 0; 128 | c = (c << 16 | c >>> 16) + d | 0; 129 | b += (c ^ d ^ a) + k[10] - 1094730640 | 0; 130 | b = (b << 23 | b >>> 9) + c | 0; 131 | a += (b ^ c ^ d) + k[13] + 681279174 | 0; 132 | a = (a << 4 | a >>> 28) + b | 0; 133 | d += (a ^ b ^ c) + k[0] - 358537222 | 0; 134 | d = (d << 11 | d >>> 21) + a | 0; 135 | c += (d ^ a ^ b) + k[3] - 722521979 | 0; 136 | c = (c << 16 | c >>> 16) + d | 0; 137 | b += (c ^ d ^ a) + k[6] + 76029189 | 0; 138 | b = (b << 23 | b >>> 9) + c | 0; 139 | a += (b ^ c ^ d) + k[9] - 640364487 | 0; 140 | a = (a << 4 | a >>> 28) + b | 0; 141 | d += (a ^ b ^ c) + k[12] - 421815835 | 0; 142 | d = (d << 11 | d >>> 21) + a | 0; 143 | c += (d ^ a ^ b) + k[15] + 530742520 | 0; 144 | c = (c << 16 | c >>> 16) + d | 0; 145 | b += (c ^ d ^ a) + k[2] - 995338651 | 0; 146 | b = (b << 23 | b >>> 9) + c | 0; 147 | // ii() 148 | a += (c ^ (b | ~d)) + k[0] - 198630844 | 0; 149 | a = (a << 6 | a >>> 26) + b | 0; 150 | d += (b ^ (a | ~c)) + k[7] + 1126891415 | 0; 151 | d = (d << 10 | d >>> 22) + a | 0; 152 | c += (a ^ (d | ~b)) + k[14] - 1416354905 | 0; 153 | c = (c << 15 | c >>> 17) + d | 0; 154 | b += (d ^ (c | ~a)) + k[5] - 57434055 | 0; 155 | b = (b << 21 | b >>> 11) + c | 0; 156 | a += (c ^ (b | ~d)) + k[12] + 1700485571 | 0; 157 | a = (a << 6 | a >>> 26) + b | 0; 158 | d += (b ^ (a | ~c)) + k[3] - 1894986606 | 0; 159 | d = (d << 10 | d >>> 22) + a | 0; 160 | c += (a ^ (d | ~b)) + k[10] - 1051523 | 0; 161 | c = (c << 15 | c >>> 17) + d | 0; 162 | b += (d ^ (c | ~a)) + k[1] - 2054922799 | 0; 163 | b = (b << 21 | b >>> 11) + c | 0; 164 | a += (c ^ (b | ~d)) + k[8] + 1873313359 | 0; 165 | a = (a << 6 | a >>> 26) + b | 0; 166 | d += (b ^ (a | ~c)) + k[15] - 30611744 | 0; 167 | d = (d << 10 | d >>> 22) + a | 0; 168 | c += (a ^ (d | ~b)) + k[6] - 1560198380 | 0; 169 | c = (c << 15 | c >>> 17) + d | 0; 170 | b += (d ^ (c | ~a)) + k[13] + 1309151649 | 0; 171 | b = (b << 21 | b >>> 11) + c | 0; 172 | a += (c ^ (b | ~d)) + k[4] - 145523070 | 0; 173 | a = (a << 6 | a >>> 26) + b | 0; 174 | d += (b ^ (a | ~c)) + k[11] - 1120210379 | 0; 175 | d = (d << 10 | d >>> 22) + a | 0; 176 | c += (a ^ (d | ~b)) + k[2] + 718787259 | 0; 177 | c = (c << 15 | c >>> 17) + d | 0; 178 | b += (d ^ (c | ~a)) + k[9] - 343485551 | 0; 179 | b = (b << 21 | b >>> 11) + c | 0; 180 | x[0] = a + x[0] | 0; 181 | x[1] = b + x[1] | 0; 182 | x[2] = c + x[2] | 0; 183 | x[3] = d + x[3] | 0; 184 | }; 185 | Md5.prototype.start = function () { 186 | this._dataLength = 0; 187 | this._bufferLength = 0; 188 | this._state.set(Md5.stateIdentity); 189 | return this; 190 | }; 191 | // Char to code point to to array conversion: 192 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt 193 | // #Example.3A_Fixing_charCodeAt_to_handle_non-Basic-Multilingual-Plane_characters_if_their_presence_earlier_in_the_string_is_unknown 194 | Md5.prototype.appendStr = function (str) { 195 | var buf8 = this._buffer8; 196 | var buf32 = this._buffer32; 197 | var bufLen = this._bufferLength; 198 | var code; 199 | var i; 200 | for (i = 0; i < str.length; i += 1) { 201 | code = str.charCodeAt(i); 202 | if (code < 128) { 203 | buf8[bufLen++] = code; 204 | } 205 | else if (code < 0x800) { 206 | buf8[bufLen++] = (code >>> 6) + 0xC0; 207 | buf8[bufLen++] = code & 0x3F | 0x80; 208 | } 209 | else if (code < 0xD800 || code > 0xDBFF) { 210 | buf8[bufLen++] = (code >>> 12) + 0xE0; 211 | buf8[bufLen++] = (code >>> 6 & 0x3F) | 0x80; 212 | buf8[bufLen++] = (code & 0x3F) | 0x80; 213 | } 214 | else { 215 | code = ((code - 0xD800) * 0x400) + (str.charCodeAt(++i) - 0xDC00) + 0x10000; 216 | if (code > 0x10FFFF) { 217 | throw new Error('Unicode standard supports code points up to U+10FFFF'); 218 | } 219 | buf8[bufLen++] = (code >>> 18) + 0xF0; 220 | buf8[bufLen++] = (code >>> 12 & 0x3F) | 0x80; 221 | buf8[bufLen++] = (code >>> 6 & 0x3F) | 0x80; 222 | buf8[bufLen++] = (code & 0x3F) | 0x80; 223 | } 224 | if (bufLen >= 64) { 225 | this._dataLength += 64; 226 | Md5._md5cycle(this._state, buf32); 227 | bufLen -= 64; 228 | buf32[0] = buf32[16]; 229 | } 230 | } 231 | this._bufferLength = bufLen; 232 | return this; 233 | }; 234 | Md5.prototype.appendAsciiStr = function (str) { 235 | var buf8 = this._buffer8; 236 | var buf32 = this._buffer32; 237 | var bufLen = this._bufferLength; 238 | var i; 239 | var j = 0; 240 | for (;;) { 241 | i = Math.min(str.length - j, 64 - bufLen); 242 | while (i--) { 243 | buf8[bufLen++] = str.charCodeAt(j++); 244 | } 245 | if (bufLen < 64) { 246 | break; 247 | } 248 | this._dataLength += 64; 249 | Md5._md5cycle(this._state, buf32); 250 | bufLen = 0; 251 | } 252 | this._bufferLength = bufLen; 253 | return this; 254 | }; 255 | Md5.prototype.appendByteArray = function (input) { 256 | var buf8 = this._buffer8; 257 | var buf32 = this._buffer32; 258 | var bufLen = this._bufferLength; 259 | var i; 260 | var j = 0; 261 | for (;;) { 262 | i = Math.min(input.length - j, 64 - bufLen); 263 | while (i--) { 264 | buf8[bufLen++] = input[j++]; 265 | } 266 | if (bufLen < 64) { 267 | break; 268 | } 269 | this._dataLength += 64; 270 | Md5._md5cycle(this._state, buf32); 271 | bufLen = 0; 272 | } 273 | this._bufferLength = bufLen; 274 | return this; 275 | }; 276 | Md5.prototype.getState = function () { 277 | var self = this; 278 | var s = self._state; 279 | return { 280 | buffer: String.fromCharCode.apply(null, self._buffer8), 281 | buflen: self._bufferLength, 282 | length: self._dataLength, 283 | state: [s[0], s[1], s[2], s[3]] 284 | }; 285 | }; 286 | Md5.prototype.setState = function (state) { 287 | var buf = state.buffer; 288 | var x = state.state; 289 | var s = this._state; 290 | var i; 291 | this._dataLength = state.length; 292 | this._bufferLength = state.buflen; 293 | s[0] = x[0]; 294 | s[1] = x[1]; 295 | s[2] = x[2]; 296 | s[3] = x[3]; 297 | for (i = 0; i < buf.length; i += 1) { 298 | this._buffer8[i] = buf.charCodeAt(i); 299 | } 300 | }; 301 | Md5.prototype.end = function (raw) { 302 | if (raw === void 0) { raw = false; } 303 | var bufLen = this._bufferLength; 304 | var buf8 = this._buffer8; 305 | var buf32 = this._buffer32; 306 | var i = (bufLen >> 2) + 1; 307 | var dataBitsLen; 308 | this._dataLength += bufLen; 309 | buf8[bufLen] = 0x80; 310 | buf8[bufLen + 1] = buf8[bufLen + 2] = buf8[bufLen + 3] = 0; 311 | buf32.set(Md5.buffer32Identity.subarray(i), i); 312 | if (bufLen > 55) { 313 | Md5._md5cycle(this._state, buf32); 314 | buf32.set(Md5.buffer32Identity); 315 | } 316 | // Do the final computation based on the tail and length 317 | // Beware that the final length may not fit in 32 bits so we take care of that 318 | dataBitsLen = this._dataLength * 8; 319 | if (dataBitsLen <= 0xFFFFFFFF) { 320 | buf32[14] = dataBitsLen; 321 | } 322 | else { 323 | var matches = dataBitsLen.toString(16).match(/(.*?)(.{0,8})$/); 324 | if (matches === null) { 325 | return; 326 | } 327 | var lo = parseInt(matches[2], 16); 328 | var hi = parseInt(matches[1], 16) || 0; 329 | buf32[14] = lo; 330 | buf32[15] = hi; 331 | } 332 | Md5._md5cycle(this._state, buf32); 333 | return raw ? this._state : Md5._hex(this._state); 334 | }; 335 | // Private Static Variables 336 | Md5.stateIdentity = new Int32Array([1732584193, -271733879, -1732584194, 271733878]); 337 | Md5.buffer32Identity = new Int32Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); 338 | Md5.hexChars = '0123456789abcdef'; 339 | Md5.hexOut = []; 340 | // Permanent instance is to use for one-call hashing 341 | Md5.onePassHasher = new Md5(); 342 | return Md5; 343 | }()); 344 | exports.Md5 = Md5; 345 | if (Md5.hashStr('hello') !== '5d41402abc4b2a76b9719d911017c592') { 346 | console.error('Md5 self test failed.'); 347 | } -------------------------------------------------------------------------------- /src/renderer/components/pages/Main.vue: -------------------------------------------------------------------------------- 1 | 64 | 180 | 495 | -------------------------------------------------------------------------------- /src/main/crypto.js: -------------------------------------------------------------------------------- 1 | import { Md5 } from "./dist/md5"; 2 | import aesjs from "aes-js"; 3 | import fs from "fs"; 4 | 5 | // Crypto 6 | var p, s0, s1, s2, s3, f = new Uint32Array([66051, 67438087]); 7 | var ecb = new aesjs.ModeOfOperation.ecb([106, 111, 54, 97, 101, 121, 54, 104, 97, 105, 100, 50, 84, 101, 105, 104]); 8 | 9 | // Consts 10 | const blockSize = 2048; 11 | const chunkSize = 6144; 12 | const decryptionChunkSize = 61440; 13 | 14 | function F(t) { 15 | var x = t >>> 24 16 | , n = t << 8 >>> 24 17 | , r = t << 16 >>> 24 18 | , e = t << 24 >>> 24; 19 | 20 | return (s0[x] + s1[n] ^ s2[r]) + s3[e]; 21 | } 22 | 23 | 24 | function decipher(t) { 25 | t[0] ^= p[17], t[1] ^= p[16], t.reverse(); 26 | for (var x = 15; x >= 0; x--) t.reverse(), t[1] ^= F(t[0]), t[0] ^= p[x] 27 | } 28 | 29 | function encipher(t) { 30 | for (var x = 0; x < 16; x++) { 31 | t[0] ^= p[x], 32 | t[1] ^= F(t[0]), 33 | t.reverse(); 34 | } 35 | 36 | t.reverse(), 37 | t[1] ^= p[16], 38 | t[0] ^= p[17] 39 | } 40 | 41 | function decryptChunk(buffer) { 42 | for (var x = new Uint32Array(2), n = new Uint32Array(2), r = f.slice(), e = 0, i = buffer.length; e < i; e += 8) { 43 | n[0] = x[0] = buffer[e + 0] << 24 | buffer[e + 1] << 16 | buffer[e + 2] << 8 | buffer[e + 3], 44 | n[1] = x[1] = buffer[e + 4] << 24 | buffer[e + 5] << 16 | buffer[e + 6] << 8 | buffer[e + 7], 45 | decipher(x), 46 | x[0] ^= r[0], 47 | x[1] ^= r[1], 48 | r[0] = n[0], 49 | r[1] = n[1], 50 | buffer[e + 0] = x[0] >> 24, 51 | buffer[e + 1] = x[0] >> 16, 52 | buffer[e + 2] = x[0] >> 8, 53 | buffer[e + 3] = x[0], 54 | buffer[e + 4] = x[1] >> 24, 55 | buffer[e + 5] = x[1] >> 16, 56 | buffer[e + 6] = x[1] >> 8, 57 | buffer[e + 7] = x[1] 58 | } 59 | } 60 | 61 | function decrypt(buffer) { 62 | for (var n = 0; n < buffer.byteLength && n + blockSize < buffer.byteLength; n += chunkSize) { 63 | decryptChunk(new Uint8Array(buffer, n, blockSize)); 64 | } 65 | 66 | return buffer; 67 | } 68 | 69 | async function decryptTrack(path, outputPath, updateProgress) { 70 | // Create a buffer to read the file and a stream to output 71 | let buffer = await fs.promises.readFile(path); 72 | let outputStream = fs.createWriteStream(outputPath); 73 | 74 | // Turn the buffer into Uint8Array 75 | buffer = new Uint8Array(buffer); 76 | 77 | // Create output buffer 78 | let output = new Uint8Array(buffer.length); 79 | 80 | // Go through the input buffer in chunks 81 | for (var i = 0; i < buffer.length; i += decryptionChunkSize) { 82 | // Calculate chunk size 83 | var chunkLength = decryptionChunkSize; 84 | if (decryptionChunkSize > buffer.length - i) 85 | chunkLength = buffer.length - i; 86 | 87 | // Move the chunk into a buffer 88 | let chunk = new Uint8Array(chunkLength); 89 | chunk.set(buffer.slice(i, i + chunkLength), 0); 90 | 91 | // Decrypt the chunk 92 | let decrypted = decrypt(chunk.buffer); 93 | 94 | // Write decrypted chunk to output stream 95 | outputStream.write(new Uint8Array(decrypted)); 96 | updateProgress(i / buffer.length); 97 | } 98 | 99 | outputStream.close(); 100 | updateProgress(1); 101 | } 102 | 103 | function initialise(id) 104 | { 105 | // Base crypto values 106 | let initB = [97, 57, 118, 48, 119, 53, 101, 103], 107 | initP = [49, 110, 102, 122, 99, 56, 108, 52], 108 | c = new Uint32Array([608135816, 2242054355, 320440878, 57701188, 2752067618, 698298832, 137296536, 3964562569, 1160258022, 953160567, 3193202383, 887688300, 3232508343, 3380367581, 1065670069, 3041331479, 2450970073, 2306472731]), 109 | a = new Uint32Array([3509652390, 2564797868, 805139163, 3491422135, 3101798381, 1780907670, 3128725573, 4046225305, 614570311, 3012652279, 134345442, 2240740374, 1667834072, 1901547113, 2757295779, 4103290238, 227898511, 1921955416, 1904987480, 2182433518, 2069144605, 3260701109, 2620446009, 720527379, 3318853667, 677414384, 3393288472, 3101374703, 2390351024, 1614419982, 1822297739, 2954791486, 3608508353, 3174124327, 2024746970, 1432378464, 3864339955, 2857741204, 1464375394, 1676153920, 1439316330, 715854006, 3033291828, 289532110, 2706671279, 2087905683, 3018724369, 1668267050, 732546397, 1947742710, 3462151702, 2609353502, 2950085171, 1814351708, 2050118529, 680887927, 999245976, 1800124847, 3300911131, 1713906067, 1641548236, 4213287313, 1216130144, 1575780402, 4018429277, 3917837745, 3693486850, 3949271944, 596196993, 3549867205, 258830323, 2213823033, 772490370, 2760122372, 1774776394, 2652871518, 566650946, 4142492826, 1728879713, 2882767088, 1783734482, 3629395816, 2517608232, 2874225571, 1861159788, 326777828, 3124490320, 2130389656, 2716951837, 967770486, 1724537150, 2185432712, 2364442137, 1164943284, 2105845187, 998989502, 3765401048, 2244026483, 1075463327, 1455516326, 1322494562, 910128902, 469688178, 1117454909, 936433444, 3490320968, 3675253459, 1240580251, 122909385, 2157517691, 634681816, 4142456567, 3825094682, 3061402683, 2540495037, 79693498, 3249098678, 1084186820, 1583128258, 426386531, 1761308591, 1047286709, 322548459, 995290223, 1845252383, 2603652396, 3431023940, 2942221577, 3202600964, 3727903485, 1712269319, 422464435, 3234572375, 1170764815, 3523960633, 3117677531, 1434042557, 442511882, 3600875718, 1076654713, 1738483198, 4213154764, 2393238008, 3677496056, 1014306527, 4251020053, 793779912, 2902807211, 842905082, 4246964064, 1395751752, 1040244610, 2656851899, 3396308128, 445077038, 3742853595, 3577915638, 679411651, 2892444358, 2354009459, 1767581616, 3150600392, 3791627101, 3102740896, 284835224, 4246832056, 1258075500, 768725851, 2589189241, 3069724005, 3532540348, 1274779536, 3789419226, 2764799539, 1660621633, 3471099624, 4011903706, 913787905, 3497959166, 737222580, 2514213453, 2928710040, 3937242737, 1804850592, 3499020752, 2949064160, 2386320175, 2390070455, 2415321851, 4061277028, 2290661394, 2416832540, 1336762016, 1754252060, 3520065937, 3014181293, 791618072, 3188594551, 3933548030, 2332172193, 3852520463, 3043980520, 413987798, 3465142937, 3030929376, 4245938359, 2093235073, 3534596313, 375366246, 2157278981, 2479649556, 555357303, 3870105701, 2008414854, 3344188149, 4221384143, 3956125452, 2067696032, 3594591187, 2921233993, 2428461, 544322398, 577241275, 1471733935, 610547355, 4027169054, 1432588573, 1507829418, 2025931657, 3646575487, 545086370, 48609733, 2200306550, 1653985193, 298326376, 1316178497, 3007786442, 2064951626, 458293330, 2589141269, 3591329599, 3164325604, 727753846, 2179363840, 146436021, 1461446943, 4069977195, 705550613, 3059967265, 3887724982, 4281599278, 3313849956, 1404054877, 2845806497, 146425753, 1854211946]), 110 | s = new Uint32Array([1266315497, 3048417604, 3681880366, 3289982499, 290971e4, 1235738493, 2632868024, 2414719590, 3970600049, 1771706367, 1449415276, 3266420449, 422970021, 1963543593, 2690192192, 3826793022, 1062508698, 1531092325, 1804592342, 2583117782, 2714934279, 4024971509, 1294809318, 4028980673, 1289560198, 2221992742, 1669523910, 35572830, 157838143, 1052438473, 1016535060, 1802137761, 1753167236, 1386275462, 3080475397, 2857371447, 1040679964, 2145300060, 2390574316, 1461121720, 2956646967, 4031777805, 4028374788, 33600511, 2920084762, 1018524850, 629373528, 3691585981, 3515945977, 2091462646, 2486323059, 586499841, 988145025, 935516892, 3367335476, 2599673255, 2839830854, 265290510, 3972581182, 2759138881, 3795373465, 1005194799, 847297441, 406762289, 1314163512, 1332590856, 1866599683, 4127851711, 750260880, 613907577, 1450815602, 3165620655, 3734664991, 3650291728, 3012275730, 3704569646, 1427272223, 778793252, 1343938022, 2676280711, 2052605720, 1946737175, 3164576444, 3914038668, 3967478842, 3682934266, 1661551462, 3294938066, 4011595847, 840292616, 3712170807, 616741398, 312560963, 711312465, 1351876610, 322626781, 1910503582, 271666773, 2175563734, 1594956187, 70604529, 3617834859, 1007753275, 1495573769, 4069517037, 2549218298, 2663038764, 504708206, 2263041392, 3941167025, 2249088522, 1514023603, 1998579484, 1312622330, 694541497, 2582060303, 2151582166, 1382467621, 776784248, 2618340202, 3323268794, 2497899128, 2784771155, 503983604, 4076293799, 907881277, 423175695, 432175456, 1378068232, 4145222326, 3954048622, 3938656102, 3820766613, 2793130115, 2977904593, 26017576, 3274890735, 3194772133, 1700274565, 1756076034, 4006520079, 3677328699, 720338349, 1533947780, 354530856, 688349552, 3973924725, 1637815568, 332179504, 3949051286, 53804574, 2852348879, 3044236432, 1282449977, 3583942155, 3416972820, 4006381244, 1617046695, 2628476075, 3002303598, 1686838959, 431878346, 2686675385, 1700445008, 1080580658, 1009431731, 832498133, 3223435511, 2605976345, 2271191193, 2516031870, 1648197032, 4164389018, 2548247927, 300782431, 375919233, 238389289, 3353747414, 2531188641, 2019080857, 1475708069, 455242339, 2609103871, 448939670, 3451063019, 1395535956, 2413381860, 1841049896, 1491858159, 885456874, 4264095073, 4001119347, 1565136089, 3898914787, 1108368660, 540939232, 1173283510, 2745871338, 3681308437, 4207628240, 3343053890, 4016749493, 1699691293, 1103962373, 3625875870, 2256883143, 3830138730, 1031889488, 3479347698, 1535977030, 4236805024, 3251091107, 2132092099, 1774941330, 1199868427, 1452454533, 157007616, 2904115357, 342012276, 595725824, 1480756522, 206960106, 497939518, 591360097, 863170706, 2375253569, 3596610801, 1814182875, 2094937945, 3421402208, 1082520231, 3463918190, 2785509508, 435703966, 3908032597, 1641649973, 2842273706, 3305899714, 1510255612, 2148256476, 2655287854, 3276092548, 4258621189, 236887753, 3681803219, 274041037, 1734335097, 3815195456, 3317970021, 1899903192, 1026095262, 4050517792, 356393447, 2410691914, 3873677099, 3682840055]), 111 | h = new Uint32Array([3913112168, 2491498743, 4132185628, 2489919796, 1091903735, 1979897079, 3170134830, 3567386728, 3557303409, 857797738, 1136121015, 1342202287, 507115054, 2535736646, 337727348, 3213592640, 1301675037, 2528481711, 1895095763, 1721773893, 3216771564, 62756741, 2142006736, 835421444, 2531993523, 1442658625, 3659876326, 2882144922, 676362277, 1392781812, 170690266, 3921047035, 1759253602, 3611846912, 1745797284, 664899054, 1329594018, 3901205900, 3045908486, 2062866102, 2865634940, 3543621612, 3464012697, 1080764994, 553557557, 3656615353, 3996768171, 991055499, 499776247, 1265440854, 648242737, 3940784050, 980351604, 3713745714, 1749149687, 3396870395, 4211799374, 3640570775, 1161844396, 3125318951, 1431517754, 545492359, 4268468663, 3499529547, 1437099964, 2702547544, 3433638243, 2581715763, 2787789398, 1060185593, 1593081372, 2418618748, 4260947970, 69676912, 2159744348, 86519011, 2512459080, 3838209314, 1220612927, 3339683548, 133810670, 1090789135, 1078426020, 1569222167, 845107691, 3583754449, 4072456591, 1091646820, 628848692, 1613405280, 3757631651, 526609435, 236106946, 48312990, 2942717905, 3402727701, 1797494240, 859738849, 992217954, 4005476642, 2243076622, 3870952857, 3732016268, 765654824, 3490871365, 2511836413, 1685915746, 3888969200, 1414112111, 2273134842, 3281911079, 4080962846, 172450625, 2569994100, 980381355, 4109958455, 2819808352, 2716589560, 2568741196, 3681446669, 3329971472, 1835478071, 660984891, 3704678404, 4045999559, 3422617507, 3040415634, 1762651403, 1719377915, 3470491036, 2693910283, 3642056355, 3138596744, 1364962596, 2073328063, 1983633131, 926494387, 3423689081, 2150032023, 4096667949, 1749200295, 3328846651, 309677260, 2016342300, 1779581495, 3079819751, 111262694, 1274766160, 443224088, 298511866, 1025883608, 3806446537, 1145181785, 168956806, 3641502830, 3584813610, 1689216846, 3666258015, 3200248200, 1692713982, 2646376535, 4042768518, 1618508792, 1610833997, 3523052358, 4130873264, 2001055236, 3610705100, 2202168115, 4028541809, 2961195399, 1006657119, 2006996926, 3186142756, 1430667929, 3210227297, 1314452623, 4074634658, 4101304120, 2273951170, 1399257539, 3367210612, 3027628629, 1190975929, 2062231137, 2333990788, 2221543033, 2438960610, 1181637006, 548689776, 2362791313, 3372408396, 3104550113, 3145860560, 296247880, 1970579870, 3078560182, 3769228297, 1714227617, 3291629107, 3898220290, 166772364, 1251581989, 493813264, 448347421, 195405023, 2709975567, 677966185, 3703036547, 1463355134, 2715995803, 1338867538, 1343315457, 2802222074, 2684532164, 233230375, 2599980071, 2000651841, 3277868038, 1638401717, 4028070440, 3237316320, 6314154, 819756386, 300326615, 590932579, 1405279636, 3267499572, 3150704214, 2428286686, 3959192993, 3461946742, 1862657033, 1266418056, 963775037, 2089974820, 2263052895, 1917689273, 448879540, 3550394620, 3981727096, 150775221, 3627908307, 1303187396, 508620638, 2975983352, 2726630617, 1817252668, 1876281319, 1457606340, 908771278, 3720792119, 3617206836, 2455994898, 1729034894, 1080033504]), 112 | v = new Uint32Array([976866871, 3556439503, 2881648439, 1522871579, 1555064734, 1336096578, 3548522304, 2579274686, 3574697629, 3205460757, 3593280638, 3338716283, 3079412587, 564236357, 2993598910, 1781952180, 1464380207, 3163844217, 3332601554, 1699332808, 1393555694, 1183702653, 3581086237, 1288719814, 691649499, 2847557200, 2895455976, 3193889540, 2717570544, 1781354906, 1676643554, 2592534050, 3230253752, 1126444790, 2770207658, 2633158820, 2210423226, 2615765581, 2414155088, 3127139286, 673620729, 2805611233, 1269405062, 4015350505, 3341807571, 4149409754, 1057255273, 2012875353, 2162469141, 2276492801, 2601117357, 993977747, 3918593370, 2654263191, 753973209, 36408145, 2530585658, 25011837, 3520020182, 2088578344, 530523599, 2918365339, 1524020338, 1518925132, 3760827505, 3759777254, 1202760957, 3985898139, 3906192525, 674977740, 4174734889, 2031300136, 2019492241, 3983892565, 4153806404, 3822280332, 352677332, 2297720250, 60907813, 90501309, 3286998549, 1016092578, 2535922412, 2839152426, 457141659, 509813237, 4120667899, 652014361, 1966332200, 2975202805, 55981186, 2327461051, 676427537, 3255491064, 2882294119, 3433927263, 1307055953, 942726286, 933058658, 2468411793, 3933900994, 4215176142, 1361170020, 2001714738, 2830558078, 3274259782, 1222529897, 1679025792, 2729314320, 3714953764, 1770335741, 151462246, 3013232138, 1682292957, 1483529935, 471910574, 1539241949, 458788160, 3436315007, 1807016891, 3718408830, 978976581, 1043663428, 3165965781, 1927990952, 4200891579, 2372276910, 3208408903, 3533431907, 1412390302, 2931980059, 4132332400, 1947078029, 3881505623, 4168226417, 2941484381, 1077988104, 1320477388, 886195818, 18198404, 3786409e3, 2509781533, 112762804, 3463356488, 1866414978, 891333506, 18488651, 661792760, 1628790961, 3885187036, 3141171499, 876946877, 2693282273, 1372485963, 791857591, 2686433993, 3759982718, 3167212022, 3472953795, 2716379847, 445679433, 3561995674, 3504004811, 3574258232, 54117162, 3331405415, 2381918588, 3769707343, 4154350007, 1140177722, 4074052095, 668550556, 3214352940, 367459370, 261225585, 2610173221, 4209349473, 3468074219, 3265815641, 314222801, 3066103646, 3808782860, 282218597, 3406013506, 3773591054, 379116347, 1285071038, 846784868, 2669647154, 3771962079, 3550491691, 2305946142, 453669953, 1268987020, 3317592352, 3279303384, 3744833421, 2610507566, 3859509063, 266596637, 3847019092, 517658769, 3462560207, 3443424879, 370717030, 4247526661, 2224018117, 4143653529, 4112773975, 2788324899, 2477274417, 1456262402, 2901442914, 1517677493, 1846949527, 2295493580, 3734397586, 2176403920, 1280348187, 1908823572, 3871786941, 846861322, 1172426758, 3287448474, 3383383037, 1655181056, 3139813346, 901632758, 1897031941, 2986607138, 3066810236, 3447102507, 1393639104, 373351379, 950779232, 625454576, 3124240540, 4148612726, 2007998917, 544563296, 2244738638, 2330496472, 2058025392, 1291430526, 424198748, 50039436, 29584100, 3605783033, 2429876329, 2791104160, 1057563949, 3255363231, 3075367218, 3463963227, 1469046755, 985887462]); 113 | 114 | // Hash ID using MD5 115 | var md5 = new Md5(); 116 | let hash = md5.appendStr(id).end(); 117 | let cryptoInput = Array.apply(null, Array(16)).map(function(item, index, array) { 118 | return hash[index].charCodeAt(0) ^ hash[index + 16].charCodeAt(0) ^ (index % 2 ? initP : initB)[array.length / 2 - 1 - Math.floor(index / 2)]; 119 | }); // x 120 | 121 | var n = new Uint32Array(2); 122 | let cryptoInputLength = cryptoInput.length; // r 123 | 124 | // Generate base p based on song ID 125 | p = c.map((t, n) => { 126 | for (var e = 0, i = 0; i < 4; i++) { 127 | e = e << 8 | cryptoInput[(4 * n + i) % cryptoInputLength]; 128 | } 129 | 130 | return t ^ e; 131 | }); 132 | 133 | s0 = a.slice(); 134 | s1 = s.slice(); 135 | s2 = h.slice(); 136 | s3 = v.slice(); 137 | 138 | // Write to p 139 | for (var e = 0; e < 18; e += 2) { 140 | encipher(n), 141 | p[e + 0] = n[0], 142 | p[e + 1] = n[1]; 143 | } 144 | 145 | for (var i = 0; i < 256; i += 2) { 146 | encipher(n), 147 | s0[i + 0] = n[0], 148 | s0[i + 1] = n[1]; 149 | } 150 | 151 | for (var _ = 0; _ < 256; _ += 2) { 152 | encipher(n), 153 | s1[_ + 0] = n[0], 154 | s1[_ + 1] = n[1]; 155 | } 156 | 157 | for (var u = 0; u < 256; u += 2) { 158 | encipher(n), 159 | s2[u + 0] = n[0], 160 | s2[u + 1] = n[1]; 161 | } 162 | 163 | for (var f = 0; f < 256; f += 2) { 164 | encipher(n), 165 | s3[f + 0] = n[0], 166 | s3[f + 1] = n[1] 167 | } 168 | } 169 | 170 | 171 | function sliceChunk(buffer, chunk, offset, start, end) { 172 | start == null && end == null || (buffer = buffer.slice ? buffer.slice(start, end) : Array.prototype.slice.call(buffer, start, end )), chunk.set(buffer, offset); 173 | } 174 | 175 | function encryptChunk(chunk) { 176 | return ecb.encrypt(chunk); 177 | } 178 | 179 | function encrypt(buffer) { 180 | // Make buffer a Uint8Array 181 | if (Array.isArray(buffer)) { 182 | buffer = new Uint8Array(buffer); 183 | } 184 | 185 | // Make sure length is divisible by 16 186 | if (buffer.length % 16 != 0) 187 | return false; 188 | 189 | // Encrypt chunks 190 | for (var destination = new Uint8Array(buffer.length), chunk = new Uint8Array(16), i = 0; i < buffer.length; i += 16) { 191 | sliceChunk(buffer, chunk, 0, i, i + 16); 192 | sliceChunk(chunk = encryptChunk(chunk), destination, i); 193 | } 194 | 195 | return destination; 196 | } 197 | 198 | function getStreamUrl(id, cdn, format, songMd5, version) { 199 | // Format 200 | // 1 = 128kbps 201 | // 3 = 320kbps 202 | // 9 = FLAC 203 | // 12 = Unknown 204 | 205 | // Get the separator and add together the data. 206 | var separator = String.fromCharCode(164); 207 | var code = [songMd5, format, id, version].join(separator); 208 | 209 | // Special MD5 hash 210 | code = Md5.onePassHasher.start().appendAsciiStr(code).end() + separator + code + separator; 211 | 212 | // Add null bytes until its length is divisible by 16 213 | code += "\0".repeat(code.length % 16 ? 16 - code.length % 16 : 0); 214 | 215 | // Get the char codes 216 | code = Array.from(code).map((char) => { 217 | return char.charCodeAt(0); 218 | }); 219 | 220 | // Encrypt and finalise string 221 | code = encrypt(code).reduce((t, x) => { 222 | return t + "0".concat(x.toString(16)).substr(-2, 2); 223 | }, ""); 224 | 225 | return cdn.replace("{0}", songMd5[0]) + code; 226 | } 227 | 228 | export default { 229 | getStreamUrl, 230 | initialise, 231 | decryptTrack 232 | }; --------------------------------------------------------------------------------