├── 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 | 
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 |
2 |
3 |
Settings will display here.
4 |
5 |
6 |
7 |
12 |
--------------------------------------------------------------------------------
/src/renderer/components/pages/Downloads.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Your downloads will display here.
4 |
5 |
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 |
2 |
3 |
You're not logged into Deezer.
4 |
Log in
5 |
6 |
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 |
2 |
7 |
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 |
2 |
3 |
4 |
5 |
6 | Freezer
7 |
8 |
9 |
10 |
11 |
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 |
2 |
3 |
4 |
5 |
6 |
11 |
12 |
13 |
14 | selectAutocomplete(item.query)">
15 |
16 | {{item.query}}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | Nothing to see here.
28 |
29 |
30 |
31 |
32 |
35 |
36 |
![]()
37 |
38 | {{item.data.title}}
39 |
40 |
41 | {{artist.name}}{{index != item.data.artists.length - 1 ? ", " : ""}}
42 |
43 |
44 |
45 |
46 |
47 |
50 |
{{item.status}}{{item.status != "Done" && item.status != "Error" ? ` (${Math.floor(item.progress * 100)}%)...` : "."}}
51 |
52 |
53 |
54 |
download(item)" :class="{'disabled': item.status != 'Idle'}">
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
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 | };
--------------------------------------------------------------------------------