├── .babelrc
├── .electron-vue
├── build.js
├── dev-client.js
├── dev-runner.js
├── webpack.main.config.js
├── webpack.renderer.config.js
└── webpack.web.config.js
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── appveyor.yml
├── dist
├── electron
│ └── .gitkeep
└── web
│ └── .gitkeep
├── doc
└── imgs
│ ├── add-new-record-1.png
│ ├── add-new-record-2.png
│ ├── record-history.png
│ ├── record-home-options.png
│ ├── video-download-home.png
│ └── video-download-record.png
├── package.json
├── src
├── helper
│ ├── IpcChannel.js
│ ├── electron-store.js
│ ├── index.js
│ ├── ipcMainUtil.js
│ └── ipcRendererUtil.js
├── index.ejs
├── main
│ ├── index.dev.js
│ └── index.js
└── renderer
│ ├── App.vue
│ ├── assets
│ └── .gitkeep
│ ├── components
│ ├── BasePage
│ │ └── Index.vue
│ ├── CommonPage
│ │ └── Index.vue
│ ├── ImageList
│ │ └── Index.vue
│ ├── Layout
│ │ ├── Index.vue
│ │ └── components
│ │ │ ├── AppMain.vue
│ │ │ └── LeftMenus.vue
│ ├── SLink
│ │ └── Index.vue
│ ├── SubPage
│ │ └── Index.vue
│ └── Views
│ │ ├── About
│ │ └── Index.vue
│ │ ├── Record
│ │ ├── Add.vue
│ │ ├── Edit.vue
│ │ ├── History.vue
│ │ ├── Index.vue
│ │ └── components
│ │ │ └── DetailForm.vue
│ │ ├── Settings
│ │ └── Index.vue
│ │ └── VideoDownload
│ │ ├── DownloadManager.vue
│ │ ├── Index.vue
│ │ └── ccomponents
│ │ ├── DownloadHistoryView.vue
│ │ ├── DownloadWaitingView.vue
│ │ └── DownloadingView.vue
│ ├── config
│ ├── SysNotice.js
│ ├── log.js
│ └── settings.js
│ ├── db
│ └── index.js
│ ├── global.js
│ ├── live-platform
│ ├── BilibiliLivePlatform.js
│ ├── DouyinLivePlatform.js
│ ├── DouyuLivePlatform.js
│ ├── HuyaLivePlatform.js
│ ├── index.js
│ └── live-platform.js
│ ├── main.js
│ ├── manager
│ ├── download-manager.js
│ ├── index.js
│ └── record-manager.js
│ ├── router
│ └── index.js
│ ├── utils
│ ├── http-util.js
│ └── validate.js
│ └── video-download-adapter
│ ├── BiliBiliVideoDownloadAdapter.js
│ ├── HuyaVideoDownloadAdapter.js
│ ├── VideoDownloadAdapter.js
│ └── index.js
└── static
├── .gitkeep
├── icon.ico
├── icon32.ico
├── styles
├── demo.css
├── demo_index.html
├── iconfont.css
├── iconfont.js
├── iconfont.json
├── iconfont.ttf
├── iconfont.woff
└── iconfont.woff2
├── tray.ico
└── tray.png
/.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 |
--------------------------------------------------------------------------------
/.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 Listr = require('listr')
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 | async 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 | const tasks = new Listr(
46 | [
47 | {
48 | title: 'building master process',
49 | task: async () => {
50 | await pack(mainConfig)
51 | .then(result => {
52 | results += result + '\n\n'
53 | })
54 | .catch(err => {
55 | console.log(`\n ${errorLog}failed to build main process`)
56 | console.error(`\n${err}\n`)
57 | })
58 | }
59 | },
60 | {
61 | title: 'building renderer process',
62 | task: async () => {
63 | await pack(rendererConfig)
64 | .then(result => {
65 | results += result + '\n\n'
66 | })
67 | .catch(err => {
68 | console.log(`\n ${errorLog}failed to build renderer process`)
69 | console.error(`\n${err}\n`)
70 | })
71 | }
72 | }
73 | ],
74 | { concurrent: 2 }
75 | )
76 |
77 | await tasks
78 | .run()
79 | .then(() => {
80 | process.stdout.write('\x1B[2J\x1B[0f')
81 | console.log(`\n\n${results}`)
82 | console.log(`${okayLog}take it away ${chalk.yellow('`electron-builder`')}\n`)
83 | process.exit()
84 | })
85 | .catch(err => {
86 | process.exit(1)
87 | })
88 | }
89 |
90 | function pack (config) {
91 | return new Promise((resolve, reject) => {
92 | config.mode = 'production'
93 | webpack(config, (err, stats) => {
94 | if (err) reject(err.stack || err)
95 | else if (stats.hasErrors()) {
96 | let err = ''
97 |
98 | stats.toString({
99 | chunks: false,
100 | colors: true
101 | })
102 | .split(/\r?\n/)
103 | .forEach(line => {
104 | err += ` ${line}\n`
105 | })
106 |
107 | reject(err)
108 | } else {
109 | resolve(stats.toString({
110 | chunks: false,
111 | colors: true
112 | }))
113 | }
114 | })
115 | })
116 | }
117 |
118 | function web () {
119 | del.sync(['dist/web/*', '!.gitkeep'])
120 | webConfig.mode = 'production'
121 | webpack(webConfig, (err, stats) => {
122 | if (err || stats.hasErrors()) console.log(err)
123 |
124 | console.log(stats.toString({
125 | chunks: false,
126 | colors: true
127 | }))
128 |
129 | process.exit()
130 | })
131 | }
132 |
133 | function greeting () {
134 | const cols = process.stdout.columns
135 | let text = ''
136 |
137 | if (cols > 85) text = 'lets-build'
138 | else if (cols > 60) text = 'lets-|build'
139 | else text = false
140 |
141 | if (text && !isCI) {
142 | say(text, {
143 | colors: ['yellow'],
144 | font: 'simple3d',
145 | space: false
146 | })
147 | } else console.log(chalk.yellow.bold('\n lets-build'))
148 | console.log()
149 | }
150 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 | hot: true,
68 | proxy: {
69 | },
70 | before (app, ctx) {
71 | // app.use(hotMiddleware)
72 | ctx.middleware.waitUntilValid(() => {
73 | resolve()
74 | })
75 | }
76 | }
77 | )
78 |
79 | server.listen(9080)
80 | })
81 | }
82 |
83 | function startMain () {
84 | return new Promise((resolve, reject) => {
85 | mainConfig.entry.main = [path.join(__dirname, '../src/main/index.dev.js')].concat(mainConfig.entry.main)
86 | mainConfig.mode = 'development'
87 | const compiler = webpack(mainConfig)
88 |
89 | compiler.hooks.watchRun.tapAsync('watch-run', (compilation, done) => {
90 | logStats('Main', chalk.white.bold('compiling...'))
91 | hotMiddleware.publish({ action: 'compiling' })
92 | done()
93 | })
94 |
95 | compiler.watch({}, (err, stats) => {
96 | if (err) {
97 | console.log(err)
98 | return
99 | }
100 |
101 | logStats('Main', stats)
102 |
103 | if (electronProcess && electronProcess.kill) {
104 | manualRestart = true
105 | process.kill(electronProcess.pid)
106 | electronProcess = null
107 | startElectron()
108 |
109 | setTimeout(() => {
110 | manualRestart = false
111 | }, 5000)
112 | }
113 |
114 | resolve()
115 | })
116 | })
117 | }
118 |
119 | function startElectron () {
120 | var args = [
121 | '--inspect=5858',
122 | path.join(__dirname, '../dist/electron/main.js')
123 | ]
124 |
125 | // detect yarn or npm and process commandline args accordingly
126 | if (process.env.npm_execpath.endsWith('yarn.js')) {
127 | args = args.concat(process.argv.slice(3))
128 | } else if (process.env.npm_execpath.endsWith('npm-cli.js')) {
129 | args = args.concat(process.argv.slice(2))
130 | }
131 |
132 | electronProcess = spawn(electron, args)
133 |
134 | electronProcess.stdout.on('data', data => {
135 | electronLog(data, 'blue')
136 | })
137 | electronProcess.stderr.on('data', data => {
138 | electronLog(data, 'red')
139 | })
140 |
141 | electronProcess.on('close', () => {
142 | if (!manualRestart) process.exit()
143 | })
144 | }
145 |
146 | function electronLog (data, color) {
147 | let log = ''
148 | data = data.toString().split(/\r?\n/)
149 | data.forEach(line => {
150 | log += ` ${line}\n`
151 | })
152 | if (/[0-9A-z]+/.test(log)) {
153 | console.log(
154 | chalk[color].bold('┏ Electron -------------------') +
155 | '\n\n' +
156 | log +
157 | chalk[color].bold('┗ ----------------------------') +
158 | '\n'
159 | )
160 | }
161 | }
162 |
163 | function greeting () {
164 | const cols = process.stdout.columns
165 | let text = ''
166 |
167 | if (cols > 104) text = 'electron-vue'
168 | else if (cols > 76) text = 'electron-|vue'
169 | else text = false
170 |
171 | if (text) {
172 | say(text, {
173 | colors: ['yellow'],
174 | font: 'simple3d',
175 | space: false
176 | })
177 | } else console.log(chalk.yellow.bold('\n electron-vue'))
178 | console.log(chalk.blue(' getting ready...') + '\n')
179 | }
180 |
181 | function init () {
182 | greeting()
183 |
184 | Promise.all([startRenderer(), startMain()])
185 | .then(() => {
186 | startElectron()
187 | })
188 | .catch(err => {
189 | console.error(err)
190 | })
191 | }
192 |
193 | init()
194 |
--------------------------------------------------------------------------------
/.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 | enforce: 'pre',
23 | exclude: /node_modules/,
24 | use: {
25 | loader: 'eslint-loader',
26 | options: {
27 | formatter: require('eslint-friendly-formatter')
28 | }
29 | }
30 | },
31 | {
32 | test: /\.js$/,
33 | use: 'babel-loader',
34 | exclude: /node_modules/
35 | },
36 | {
37 | test: /\.node$/,
38 | use: 'node-loader'
39 | }
40 | ]
41 | },
42 | node: {
43 | __dirname: process.env.NODE_ENV !== 'production',
44 | __filename: process.env.NODE_ENV !== 'production'
45 | },
46 | output: {
47 | filename: '[name].js',
48 | libraryTarget: 'commonjs2',
49 | path: path.join(__dirname, '../dist/electron')
50 | },
51 | plugins: [
52 | new webpack.NoEmitOnErrorsPlugin()
53 | ],
54 | resolve: {
55 | extensions: ['.js', '.json', '.node']
56 | },
57 | target: 'electron-main'
58 | }
59 |
60 | /**
61 | * Adjust mainConfig for development Settings
62 | */
63 | if (process.env.NODE_ENV !== 'production') {
64 | mainConfig.plugins.push(
65 | new webpack.DefinePlugin({
66 | '__static': `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"`
67 | })
68 | )
69 | }
70 |
71 | /**
72 | * Adjust mainConfig for production Settings
73 | */
74 | if (process.env.NODE_ENV === 'production') {
75 | mainConfig.plugins.push(
76 | new MinifyPlugin(),
77 | new webpack.DefinePlugin({
78 | 'process.env.NODE_ENV': '"production"'
79 | })
80 | )
81 | }
82 |
83 | module.exports = mainConfig
84 |
--------------------------------------------------------------------------------
/.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 | // 解决element-ui el-table有数据的时候渲染不出来
23 | let whiteListedModules = ['vue', 'element-ui']
24 |
25 | let rendererConfig = {
26 | devtool: '#cheap-module-eval-source-map',
27 | entry: {
28 | renderer: path.join(__dirname, '../src/renderer/main.js')
29 | },
30 | externals: [
31 | ...Object.keys(dependencies || {}).filter(d => !whiteListedModules.includes(d))
32 | ],
33 | module: {
34 | rules: [
35 | {
36 | test: /\.(js|vue)$/,
37 | enforce: 'pre',
38 | exclude: /node_modules/,
39 | use: {
40 | loader: 'eslint-loader',
41 | options: {
42 | formatter: require('eslint-friendly-formatter')
43 | }
44 | }
45 | },
46 | {
47 | test: /\.scss$/,
48 | use: ['vue-style-loader', 'css-loader', 'sass-loader']
49 | },
50 | {
51 | test: /\.sass$/,
52 | use: ['vue-style-loader', 'css-loader', 'sass-loader?indentedSyntax']
53 | },
54 | {
55 | test: /\.less$/,
56 | use: ['vue-style-loader', 'css-loader', 'less-loader']
57 | },
58 | {
59 | test: /\.css$/,
60 | use: ['vue-style-loader', 'css-loader']
61 | },
62 | {
63 | test: /\.html$/,
64 | use: 'vue-html-loader'
65 | },
66 | {
67 | test: /\.js$/,
68 | use: 'babel-loader',
69 | exclude: /node_modules/
70 | },
71 | {
72 | test: /\.node$/,
73 | use: 'node-loader'
74 | },
75 | {
76 | test: /\.vue$/,
77 | use: {
78 | loader: 'vue-loader',
79 | options: {
80 | extractCSS: process.env.NODE_ENV === 'production',
81 | loaders: {
82 | sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1',
83 | scss: 'vue-style-loader!css-loader!sass-loader',
84 | less: 'vue-style-loader!css-loader!less-loader'
85 | }
86 | }
87 | }
88 | },
89 | {
90 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
91 | use: {
92 | loader: 'url-loader',
93 | query: {
94 | limit: 10000,
95 | name: 'imgs/[name]--[folder].[ext]'
96 | }
97 | }
98 | },
99 | {
100 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
101 | loader: 'url-loader',
102 | options: {
103 | limit: 10000,
104 | name: 'media/[name]--[folder].[ext]'
105 | }
106 | },
107 | {
108 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
109 | use: {
110 | loader: 'url-loader',
111 | query: {
112 | limit: 10000,
113 | name: 'fonts/[name]--[folder].[ext]'
114 | }
115 | }
116 | }
117 | ]
118 | },
119 | node: {
120 | __dirname: process.env.NODE_ENV !== 'production',
121 | __filename: process.env.NODE_ENV !== 'production'
122 | },
123 | plugins: [
124 | new VueLoaderPlugin(),
125 | new MiniCssExtractPlugin({filename: 'styles.css'}),
126 | new HtmlWebpackPlugin({
127 | filename: 'index.html',
128 | template: path.resolve(__dirname, '../src/index.ejs'),
129 | templateParameters(compilation, assets, options) {
130 | return {
131 | compilation: compilation,
132 | webpack: compilation.getStats().toJson(),
133 | webpackConfig: compilation.options,
134 | htmlWebpackPlugin: {
135 | files: assets,
136 | options: options,
137 | },
138 | process,
139 | };
140 | },
141 | minify: {
142 | collapseWhitespace: true,
143 | removeAttributeQuotes: true,
144 | removeComments: true
145 | },
146 | nodeModules: process.env.NODE_ENV !== 'production'
147 | ? path.resolve(__dirname, '../node_modules')
148 | : false
149 | }),
150 | new webpack.NoEmitOnErrorsPlugin()
151 | ],
152 | output: {
153 | filename: '[name].js',
154 | libraryTarget: 'commonjs2',
155 | path: path.join(__dirname, '../dist/electron')
156 | },
157 | resolve: {
158 | alias: {
159 | '@': path.join(__dirname, '../src/renderer'),
160 | 'vue$': 'vue/dist/vue.esm.js'
161 | },
162 | extensions: ['.js', '.vue', '.json', '.css', '.node']
163 | },
164 | target: 'electron-renderer'
165 | }
166 |
167 | /**
168 | * Adjust rendererConfig for development Settings
169 | */
170 | if (process.env.NODE_ENV !== 'production') {
171 | rendererConfig.plugins.push(
172 | new webpack.HotModuleReplacementPlugin(),
173 | new webpack.DefinePlugin({
174 | '__static': `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"`
175 | })
176 | )
177 | }
178 |
179 | /**
180 | * Adjust rendererConfig for production Settings
181 | */
182 | if (process.env.NODE_ENV === 'production') {
183 | rendererConfig.devtool = ''
184 |
185 | rendererConfig.plugins.push(
186 | new MinifyPlugin(),
187 | new CopyWebpackPlugin([
188 | {
189 | from: path.join(__dirname, '../static'),
190 | to: path.join(__dirname, '../dist/electron/static'),
191 | ignore: ['.*']
192 | }
193 | ]),
194 | new webpack.DefinePlugin({
195 | 'process.env.NODE_ENV': '"production"'
196 | }),
197 | new webpack.LoaderOptionsPlugin({
198 | minimize: true
199 | })
200 | )
201 | }
202 |
203 | module.exports = rendererConfig
204 |
--------------------------------------------------------------------------------
/.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: /\.(js|vue)$/,
23 | enforce: 'pre',
24 | exclude: /node_modules/,
25 | use: {
26 | loader: 'eslint-loader',
27 | options: {
28 | formatter: require('eslint-friendly-formatter')
29 | }
30 | }
31 | },
32 | {
33 | test: /\.scss$/,
34 | use: ['vue-style-loader', 'css-loader', 'sass-loader']
35 | },
36 | {
37 | test: /\.sass$/,
38 | use: ['vue-style-loader', 'css-loader', 'sass-loader?indentedSyntax']
39 | },
40 | {
41 | test: /\.less$/,
42 | use: ['vue-style-loader', 'css-loader', 'less-loader']
43 | },
44 | {
45 | test: /\.css$/,
46 | use: ['vue-style-loader', 'css-loader']
47 | },
48 | {
49 | test: /\.html$/,
50 | use: 'vue-html-loader'
51 | },
52 | {
53 | test: /\.js$/,
54 | use: 'babel-loader',
55 | include: [ path.resolve(__dirname, '../src/renderer') ],
56 | exclude: /node_modules/
57 | },
58 | {
59 | test: /\.vue$/,
60 | use: {
61 | loader: 'vue-loader',
62 | options: {
63 | extractCSS: true,
64 | loaders: {
65 | sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1',
66 | scss: 'vue-style-loader!css-loader!sass-loader',
67 | less: 'vue-style-loader!css-loader!less-loader'
68 | }
69 | }
70 | }
71 | },
72 | {
73 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
74 | use: {
75 | loader: 'url-loader',
76 | query: {
77 | limit: 10000,
78 | name: 'imgs/[name].[ext]'
79 | }
80 | }
81 | },
82 | {
83 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
84 | use: {
85 | loader: 'url-loader',
86 | query: {
87 | limit: 10000,
88 | name: 'fonts/[name].[ext]'
89 | }
90 | }
91 | }
92 | ]
93 | },
94 | plugins: [
95 | new VueLoaderPlugin(),
96 | new MiniCssExtractPlugin({filename: 'styles.css'}),
97 | new HtmlWebpackPlugin({
98 | filename: 'index.html',
99 | template: path.resolve(__dirname, '../src/index.ejs'),
100 | templateParameters(compilation, assets, options) {
101 | return {
102 | compilation: compilation,
103 | webpack: compilation.getStats().toJson(),
104 | webpackConfig: compilation.options,
105 | htmlWebpackPlugin: {
106 | files: assets,
107 | options: options,
108 | },
109 | process,
110 | };
111 | },
112 | minify: {
113 | collapseWhitespace: true,
114 | removeAttributeQuotes: true,
115 | removeComments: true
116 | },
117 | nodeModules: false
118 | }),
119 | new webpack.DefinePlugin({
120 | 'process.env.IS_WEB': 'true'
121 | }),
122 | new webpack.HotModuleReplacementPlugin(),
123 | new webpack.NoEmitOnErrorsPlugin()
124 | ],
125 | output: {
126 | filename: '[name].js',
127 | path: path.join(__dirname, '../dist/web')
128 | },
129 | resolve: {
130 | alias: {
131 | '@': path.join(__dirname, '../src/renderer'),
132 | 'vue$': 'vue/dist/vue.esm.js'
133 | },
134 | extensions: ['.js', '.vue', '.json', '.css']
135 | },
136 | target: 'web'
137 | }
138 |
139 | /**
140 | * Adjust webConfig for production Settings
141 | */
142 | if (process.env.NODE_ENV === 'production') {
143 | webConfig.devtool = ''
144 |
145 | webConfig.plugins.push(
146 | new MinifyPlugin(),
147 | new CopyWebpackPlugin([
148 | {
149 | from: path.join(__dirname, '../static'),
150 | to: path.join(__dirname, '../dist/web/static'),
151 | ignore: ['.*']
152 | }
153 | ]),
154 | new webpack.DefinePlugin({
155 | 'process.env.NODE_ENV': '"production"'
156 | }),
157 | new webpack.LoaderOptionsPlugin({
158 | minimize: true
159 | })
160 | )
161 | }
162 |
163 | module.exports = webConfig
164 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zbfzn/MultiPlatformLiveVideoRecorder/502d4757d026a06cebf5681387fcd7380e77c4b7/.eslintignore
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: 'babel-eslint',
4 | parserOptions: {
5 | sourceType: 'module'
6 | },
7 | env: {
8 | browser: true,
9 | node: true
10 | },
11 | extends: 'standard',
12 | globals: {
13 | __static: true
14 | },
15 | plugins: [
16 | 'html'
17 | ],
18 | 'rules': {
19 | // allow paren-less arrow functions
20 | 'arrow-parens': 0,
21 | // allow async-await
22 | 'generator-star-spacing': 0,
23 | // allow debugger during development
24 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/*
2 | .DS_Store
3 | dist/electron/*
4 | dist/web/*
5 | build/*
6 | !build/icons
7 | node_modules/
8 | npm-debug.log
9 | npm-debug.log.*
10 | thumbs.db
11 | !.gitkeep
12 |
--------------------------------------------------------------------------------
/.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 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install git-lfs; fi
23 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install --no-install-recommends -y icnsutils graphicsmagick xz-utils; fi
24 | install:
25 | - nvm install 10
26 | - curl -o- -L https://yarnpkg.com/install.sh | bash
27 | - source ~/.bashrc
28 | - npm install -g xvfb-maybe
29 | - yarn
30 | before_script:
31 | - git lfs pull
32 | script:
33 | - yarn run build
34 | branches:
35 | only:
36 | - master
37 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MultiPlatformLiveVideoRecorder
2 |
3 | > 多平台直播录制
4 |
5 | - [目录](#multiplatformlivevideorecorder)
6 | - [一、功能介绍](#%20%E4%B8%80%E3%80%81%E5%8A%9F%E8%83%BD%E4%BB%8B%E7%BB%8D)
7 | - [二、使用](#%E4%BA%8C%E4%BD%BF%E7%94%A8)
8 | - [1、自动录制](#1%E8%87%AA%E5%8A%A8%E5%BD%95%E5%88%B6)
9 | - [2、视频下载](#2%E8%A7%86%E9%A2%91%E4%B8%8B%E8%BD%BD)
10 | - [三、构建](#%E4%B8%89%E6%9E%84%E5%BB%BA)
11 |
12 | #### 一、功能介绍
13 |
14 | 1、**直播录制**
15 |
16 | (1)、支持斗鱼、虎牙、哔哩哔哩、抖音平台
17 |
18 | (2)、支持开播检查并录制
19 |
20 | (3)、支持录制视频分片
21 |
22 | (4)、支持多种输出格式
23 |
24 | (5)、支持录制历史查看、打标签
25 |
26 | 2、**视频下载**
27 |
28 | (1)、支持虎牙、哔哩哔哩用户视频下载
29 |
30 | (2)、支持多种输出格式
31 |
32 | (3)、支持批量下载用户全部视频
33 |
34 | (4)、支持视频下载链接格式
35 |
36 | | 平台 | 支持链接 |
37 | | ---- | ------------------------------------------------------------------------------------------------------------------------------------------ |
38 | | 哔哩哔哩 | 用户主页:https://space.bilibili.com/xxx
视频播放页:https://www.bilibili.com/video/BVxxxxx
播放列表:https://www.bilibili.com/medialist/play/xxx |
39 | | 虎牙 | 用户视频主页:https://v.huya.com/u/xxx
视频播放页:https://v.huya.com/play/xxx.html |
40 |
41 | #### 二、使用
42 |
43 | ##### 1、自动录制
44 |
45 | (1)、添加直播间信息
46 | 
47 | 
48 |
49 | | 配置 | 说明 |
50 | | ----- | ------------------------------------------------------------- |
51 | | ID | 用户id(抖音)、直播间地址、房间号 |
52 | | 昵称 | 粘贴ID后自动解析,保存后不可修改 |
53 | | 清晰度 | 录制时选择的最高清晰度(没有则使用当前最高清晰度) |
54 | | 自动检查 | 定时检查直播间状态,开播则录制(全局自动检查开启下有效) |
55 | | 检查窗口 | 下一次检查开播状态间隔时间(自动检查开启下有效) |
56 | | 自动分片 | 录制时是否按照给定分片时长分段录制;**禁用分片设置立即生效,正在录制的任务将不再分片;启用分片设置将在下次录制时生效** |
57 | | 分片时长 | 每段视频最长时长(自动分片开启下有效) |
58 | | 格式 | 录制视频输出格式 |
59 | | 输出文件夹 | 录制视频保存目录,实际保存目录:**${选择的目录}/${直播平台名称}/${昵称}** |
60 |
61 | (2)、录制任务界面
62 | 
63 |
64 | (3)、录制记录
65 | 
66 |
67 | ##### 2、视频下载
68 |
69 | 
70 | 
71 |
72 | > **注:下载哔哩哔哩高清晰度视频需配置用户cookie(‘软件设置’页面设置)**
73 |
74 | #### 三、构建
75 |
76 | ```text
77 | # install
78 | npm install
79 |
80 | # run
81 | npm run dev
82 |
83 | # build
84 | npm run build:full
85 | ```
86 |
87 | > **注意:本软件仅供学习和交流,禁止用于商业用途。**
88 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/dist/electron/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zbfzn/MultiPlatformLiveVideoRecorder/502d4757d026a06cebf5681387fcd7380e77c4b7/dist/electron/.gitkeep
--------------------------------------------------------------------------------
/dist/web/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zbfzn/MultiPlatformLiveVideoRecorder/502d4757d026a06cebf5681387fcd7380e77c4b7/dist/web/.gitkeep
--------------------------------------------------------------------------------
/doc/imgs/add-new-record-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zbfzn/MultiPlatformLiveVideoRecorder/502d4757d026a06cebf5681387fcd7380e77c4b7/doc/imgs/add-new-record-1.png
--------------------------------------------------------------------------------
/doc/imgs/add-new-record-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zbfzn/MultiPlatformLiveVideoRecorder/502d4757d026a06cebf5681387fcd7380e77c4b7/doc/imgs/add-new-record-2.png
--------------------------------------------------------------------------------
/doc/imgs/record-history.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zbfzn/MultiPlatformLiveVideoRecorder/502d4757d026a06cebf5681387fcd7380e77c4b7/doc/imgs/record-history.png
--------------------------------------------------------------------------------
/doc/imgs/record-home-options.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zbfzn/MultiPlatformLiveVideoRecorder/502d4757d026a06cebf5681387fcd7380e77c4b7/doc/imgs/record-home-options.png
--------------------------------------------------------------------------------
/doc/imgs/video-download-home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zbfzn/MultiPlatformLiveVideoRecorder/502d4757d026a06cebf5681387fcd7380e77c4b7/doc/imgs/video-download-home.png
--------------------------------------------------------------------------------
/doc/imgs/video-download-record.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zbfzn/MultiPlatformLiveVideoRecorder/502d4757d026a06cebf5681387fcd7380e77c4b7/doc/imgs/video-download-record.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "MultiPlatformLiveVideoRecorder",
3 | "version": "1.1.1-alpha",
4 | "author": "ZuoBro ",
5 | "description": "多平台聚合直播录制",
6 | "license": "MIT",
7 | "main": "./dist/electron/main.js",
8 | "scripts": {
9 | "build": "node .electron-vue/build.js && electron-builder",
10 | "build:dir": "node .electron-vue/build.js && electron-builder --dir",
11 | "build:clean": "cross-env BUILD_TARGET=clean node .electron-vue/build.js",
12 | "build:web": "cross-env BUILD_TARGET=web node .electron-vue/build.js",
13 | "build:full": "cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.main.config.js && cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.renderer.config.js && node .electron-vue/build.js && electron-builder",
14 | "dev": "node .electron-vue/dev-runner.js",
15 | "lint": "eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter src",
16 | "lint:fix": "eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter --fix src",
17 | "pack": "npm run pack:main && npm run pack:renderer",
18 | "pack:main": "cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.main.config.js",
19 | "pack:renderer": "cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.renderer.config.js",
20 | "postinstall": "npm run lint:fix"
21 | },
22 | "build": {
23 | "productName": "MPLVR 多平台聚合直播录制",
24 | "appId": "top.lyfzn.mplvr",
25 | "directories": {
26 | "output": "build"
27 | },
28 | "files": [
29 | "dist/electron/**/*"
30 | ],
31 | "nsis": {
32 | "oneClick": false,
33 | "allowToChangeInstallationDirectory": true
34 | },
35 | "dmg": {
36 | "contents": [
37 | {
38 | "x": 410,
39 | "y": 150,
40 | "type": "link",
41 | "path": "/Applications"
42 | },
43 | {
44 | "x": 130,
45 | "y": 150,
46 | "type": "file"
47 | }
48 | ]
49 | },
50 | "mac": {
51 | "icon": "build/icons/icon.icns"
52 | },
53 | "win": {
54 | "icon": "./static/icon.ico",
55 | "requestedExecutionLevel": "highestAvailable"
56 | },
57 | "linux": {
58 | "icon": "./static/icon.ico"
59 | },
60 | "electronDownload": {
61 | "mirror": "https://npm.taobao.org/mirrors/electron/"
62 | }
63 | },
64 | "dependencies": {
65 | "axios": "^0.24.0",
66 | "body-parser": "^1.19.1",
67 | "crypto-js": "^4.1.1",
68 | "element-ui": "^2.15.6",
69 | "express": "^4.17.2",
70 | "ffmpeg-static": "^2.4.0",
71 | "fluent-ffmpeg": "^2.1.2",
72 | "fs": "^0.0.1-security",
73 | "fs-extra": "^10.0.0",
74 | "linkedom": "^0.14.9",
75 | "log4js": "^6.3.0",
76 | "nedb": "^1.8.0",
77 | "query-string": "^7.0.1",
78 | "readline": "^1.3.0",
79 | "request": "^2.88.2",
80 | "uuid": "^3.3.2",
81 | "vm2": "^3.8.1",
82 | "vue": "^2.5.16",
83 | "vue-electron": "^1.0.6",
84 | "vue-router": "^3.0.1",
85 | "vuex": "^3.0.1",
86 | "vuex-electron": "^1.0.3"
87 | },
88 | "devDependencies": {
89 | "@vue/cli-plugin-babel": "4.4.4",
90 | "ajv": "^6.5.0",
91 | "babel-core": "^6.26.3",
92 | "babel-eslint": "^8.2.3",
93 | "babel-loader": "^7.1.4",
94 | "babel-minify-webpack-plugin": "^0.3.1",
95 | "babel-plugin-transform-runtime": "^6.23.0",
96 | "babel-preset-env": "^1.7.0",
97 | "babel-preset-stage-0": "^6.24.1",
98 | "babel-register": "^6.26.0",
99 | "bufferutil": "^4.0.5",
100 | "cfonts": "^2.1.2",
101 | "chalk": "^2.4.1",
102 | "copy-webpack-plugin": "^4.5.1",
103 | "cross-env": "^5.1.6",
104 | "css-loader": "^0.28.11",
105 | "del": "^3.0.0",
106 | "devtron": "^1.4.0",
107 | "electron": "^2.0.4",
108 | "electron-builder": "^22.14.5",
109 | "electron-debug": "^1.5.0",
110 | "electron-devtools-installer": "^2.2.4",
111 | "eslint": "^4.19.1",
112 | "eslint-config-standard": "^11.0.0",
113 | "eslint-friendly-formatter": "^4.0.1",
114 | "eslint-loader": "^2.0.0",
115 | "eslint-plugin-html": "^4.0.3",
116 | "eslint-plugin-import": "^2.12.0",
117 | "eslint-plugin-node": "^6.0.1",
118 | "eslint-plugin-promise": "^3.8.0",
119 | "eslint-plugin-standard": "^3.1.0",
120 | "file-loader": "^1.1.11",
121 | "html-webpack-plugin": "^3.2.0",
122 | "listr": "^0.14.3",
123 | "mini-css-extract-plugin": "0.4.0",
124 | "node-loader": "^0.6.0",
125 | "node-sass": "^4.9.2",
126 | "sass-loader": "^7.0.3",
127 | "style-loader": "^0.21.0",
128 | "url-loader": "^1.0.1",
129 | "utf-8-validate": "^5.0.7",
130 | "vue-html-loader": "^1.2.4",
131 | "vue-loader": "^15.2.4",
132 | "vue-style-loader": "^4.1.0",
133 | "vue-template-compiler": "^2.6.10",
134 | "webpack": "^4.15.1",
135 | "webpack-cli": "^3.0.8",
136 | "webpack-dev-server": "^3.1.4",
137 | "webpack-hot-middleware": "^2.22.2",
138 | "webpack-merge": "^4.1.3"
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/src/helper/IpcChannel.js:
--------------------------------------------------------------------------------
1 | // app进程和renderer进程通信channel定义
2 | const IpcChannel = {
3 | stopAllTasks: 'stopAllTasks',
4 | getDouyinRoomData: 'getDouyinRoomData',
5 | getDouyinRoomDataReply: 'getDouyinRoomDataReply'
6 | }
7 |
8 | export default IpcChannel
9 |
--------------------------------------------------------------------------------
/src/helper/electron-store.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import path from 'path'
3 | import CryptoJS from 'crypto-js'
4 |
5 | // 十六位十六进制数作为密钥
6 | const aesKey = CryptoJS.enc.Utf8.parse('C23FF23A12ABCDBF')
7 | // 十六位十六进制数作为密钥偏移量
8 | const aesIv = CryptoJS.enc.Utf8.parse('ABCDEF1234123412')
9 | // 解密方法
10 | function aesDecrypt (word) {
11 | let encryptedHexStr = CryptoJS.enc.Hex.parse(word)
12 | let srcs = CryptoJS.enc.Base64.stringify(encryptedHexStr)
13 | let decrypt = CryptoJS.AES.decrypt(srcs, aesKey, { iv: aesIv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 })
14 | let decryptedStr = decrypt.toString(CryptoJS.enc.Utf8)
15 | return decryptedStr.toString()
16 | }
17 |
18 | // 加密方法
19 | function aesEncrypt (word) {
20 | let srcs = CryptoJS.enc.Utf8.parse(word)
21 | let encrypted = CryptoJS.AES.encrypt(srcs, aesKey, { iv: aesIv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 })
22 | return encrypted.ciphertext.toString().toUpperCase()
23 | }
24 | // electron store,简单的存储类
25 | class ElectronStore {
26 | constructor (app) {
27 | if (!app) {
28 | const electron = require('electron')
29 | if (electron.app) {
30 | // store 在main中调用
31 | app = electron.app
32 | } else {
33 | // store 在render中调用
34 | app = electron.remote.app
35 | }
36 | }
37 | const storeDir = app.getPath('userData')
38 | this._storeName = 'store.json'
39 | this._storeData = {}
40 | const fullPath = path.join(storeDir, this._storeName)
41 | this._storePath = fullPath
42 | this.mkdirs(storeDir)
43 | if (!fs.existsSync(fullPath)) {
44 | this.saveStore()
45 | } else {
46 | const content = fs.readFileSync(this._storePath, {encoding: 'utf-8'})
47 | if (!content || content.trim() === '') {
48 | this.saveStore()
49 | } else {
50 | this.getStore()
51 | }
52 | }
53 | }
54 | saveStore () {
55 | fs.writeFileSync(this._storePath, aesEncrypt(JSON.stringify(this._storeData)), {encoding: 'utf-8'})
56 | }
57 | getStore () {
58 | const text = fs.readFileSync(this._storePath, {encoding: 'utf-8'})
59 | this._storeData = JSON.parse(aesDecrypt(text))
60 | return this._storeData
61 | }
62 | mkdirs (dirs) {
63 | if (fs.existsSync(dirs)) {
64 | return true
65 | } else {
66 | if (this.mkdirs(path.dirname(dirs))) {
67 | fs.mkdirSync(dirs)
68 | }
69 | }
70 | }
71 | get (key, def) {
72 | const value = this._storeData[key]
73 | if (!value && def) {
74 | return def
75 | }
76 | return value
77 | }
78 | set (key, value) {
79 | this._storeData[key] = value
80 | this.saveStore()
81 | }
82 | clear () {
83 | this._storeData = {}
84 | this.saveStore()
85 | }
86 | // 静态调用
87 | static store
88 | static initStore () {
89 | if (!this.store) {
90 | this.store = new ElectronStore()
91 | }
92 | }
93 | static get (key, def) {
94 | this.initStore()
95 | return this.store.get(key, def)
96 | }
97 | static set (key, value) {
98 | this.initStore()
99 | this.store.set(key, value)
100 | }
101 | static clear () {
102 | this.initStore()
103 | this.store.clear()
104 | }
105 | }
106 | export default ElectronStore
107 |
--------------------------------------------------------------------------------
/src/helper/index.js:
--------------------------------------------------------------------------------
1 | const { version, build } = require('../../package.json')
2 |
3 | export const winURL = process.env.NODE_ENV === 'development'
4 | ? `http://localhost:9080`
5 | : `file://${__dirname}/index.html`
6 |
7 | /**
8 | * @param time 单位ms
9 | * @return {Promise}
10 | */
11 | export function sleep (time) {
12 | return new Promise(resolve => setTimeout(resolve, time))
13 | }
14 |
15 | /**
16 | * 根据不同平台移除路径非法字符
17 | * @param str
18 | * @return {string|*}
19 | */
20 | export function illegalPathCharRemove (str) {
21 | if (typeof str !== 'string') return str
22 | switch (process.platform) {
23 | case 'win32':
24 | str = str.replace(/[<>:"|?*./\\]/g, '')
25 | break
26 | case 'linux':
27 | str = str.replace(/\./g, '')
28 | break
29 | case 'darwin':
30 | str = str.replace(/:/g, '')
31 | break
32 | }
33 | return str
34 | }
35 |
36 | export function getAppName () {
37 | return `${build.productName} - v${version}`
38 | }
39 |
40 | export function getProductVersion () {
41 | return version
42 | }
43 |
44 | export function getProductName () {
45 | return build.productName
46 | }
47 |
48 | export function getGatewayExchangeServerPort () {
49 | return process.env.NODE_ENV === 'development' ? 31091 : 32999
50 | }
51 |
--------------------------------------------------------------------------------
/src/helper/ipcMainUtil.js:
--------------------------------------------------------------------------------
1 | import {BrowserWindow, ipcMain} from 'electron'
2 | import IpcChannel from './IpcChannel'
3 | // 主进程进程通信工具
4 | const IpcMainUtil = {
5 | stopAllTasks: () => {
6 | BrowserWindow.getAllWindows()
7 | .forEach(window_ => {
8 | window_.webContents.send(IpcChannel.stopAllTasks, 'stopAllTasks')
9 | })
10 | },
11 | sendToRenderer: (window, channel, ...args) => {
12 | window.webContents.send(channel, ...args)
13 | },
14 | on: (channel, listener) => {
15 | ipcMain.on(channel, listener)
16 | },
17 | removeAllListeners: (channel) => {
18 | ipcMain.removeAllListeners(channel)
19 | },
20 | removeListener: (channel, listener) => {
21 | ipcMain.removeListener(channel, listener)
22 | }
23 | }
24 | export default IpcMainUtil
25 |
--------------------------------------------------------------------------------
/src/helper/ipcRendererUtil.js:
--------------------------------------------------------------------------------
1 | import {ipcRenderer} from 'electron'
2 | import IpcChannel from './IpcChannel'
3 |
4 | // Renderer进程通信工具
5 | const ipcRendererUtil = {
6 | onStopAllTasks: (listener) => {
7 | // 必须先移除所有监听器,避免重复监听
8 | ipcRenderer.removeAllListeners(IpcChannel.stopAllTasks)
9 | ipcRenderer.on(IpcChannel.stopAllTasks, listener)
10 | },
11 | sendToMain: (channel, ...args) => {
12 | ipcRenderer.send(channel, ...args)
13 | },
14 | on: (channel, listener) => {
15 | ipcRenderer.on(channel, listener)
16 | },
17 | removeAllListeners: (channel) => {
18 | ipcRenderer.removeAllListeners(channel)
19 | },
20 | removeListener: (channel, listener) => {
21 | ipcRenderer.removeListener(channel, listener)
22 | }
23 | }
24 |
25 | export default ipcRendererUtil
26 |
--------------------------------------------------------------------------------
/src/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | live-video-recorder
8 | <% if (htmlWebpackPlugin.options.nodeModules) { %>
9 |
10 |
13 | <% } %>
14 |
15 |
16 |
17 |
18 | <% if (!process.browser) { %>
19 |
22 | <% } %>
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/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: true })
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/main/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import {app, BrowserWindow, Menu, Tray} from 'electron'
4 | import path from 'path'
5 | import fs from 'fs'
6 | import express from 'express'
7 | import bodyParser from 'body-parser'
8 | import axios from 'axios'
9 |
10 | import IpcMainUtil from '../helper/ipcMainUtil'
11 | import IpcChannel from '../helper/IpcChannel'
12 | import {getAppName, getGatewayExchangeServerPort} from '../helper'
13 | /**
14 | * Set `__static` path to static files in production
15 | * https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-static-assets.html
16 | */
17 | if (process.env.NODE_ENV !== 'development') {
18 | global.__static = require('path').join(__dirname, '/static').replace(/\\/g, '\\\\')
19 | }
20 | // 全局错误捕捉
21 | const errorHandler = (err) => {
22 | console.error('uncaught', err)
23 | fs.writeFileSync(`uncaught-${Date.now()}.log`, err.stack)
24 | }
25 | process.on('uncaughtException', errorHandler)
26 | process.on('unhandledRejection', errorHandler)
27 |
28 | let mainWindow
29 | let tray = null
30 | const winURL = process.env.NODE_ENV === 'development'
31 | ? `http://localhost:9080`
32 | : `file://${__dirname}/index.html`
33 | // 单例模式
34 | const isSecondInstance = app.makeSingleInstance((commandLine, workingDirectory) => {
35 | // Someone tried to run a second instance, we should focus our window.
36 | if (mainWindow) {
37 | if (mainWindow.isMinimized()) mainWindow.restore()
38 | mainWindow.focus()
39 | }
40 | })
41 | // 第二个实例停止
42 | if (isSecondInstance) {
43 | app.quit()
44 | }
45 |
46 | function createWindow () {
47 | /**
48 | * Initial window options
49 | */
50 | mainWindow = new BrowserWindow({
51 | height: 830,
52 | useContentSize: true,
53 | width: 1680,
54 | show: false,
55 | icon: path.join(__dirname, 'static/icon32.ico'),
56 | // 关闭web安全(允许跨域)
57 | webPreferences: {
58 | webSecurity: false,
59 | nodeIntegration: true,
60 | contextIsolation: false
61 | }
62 | })
63 | mainWindow.appTray = tray
64 | // mainWindow.setMenu(null)
65 | // 打开开发者工具(调试使用)
66 | // mainWindow.webContents.openDevTools()
67 | mainWindow.loadURL(winURL)
68 |
69 | mainWindow.on('ready-to-show', () => {
70 | mainWindow.setTitle(getAppName())
71 | // mainWindow.webContents.openDevTools()
72 | mainWindow.show()
73 | })
74 | mainWindow.on('close', event => {
75 | event.preventDefault()
76 | // 有正在进行中的任务,询问是否停止后关闭,关闭过程交由Renderer
77 | IpcMainUtil.stopAllTasks()
78 | })
79 | mainWindow.on('closed', () => {
80 | tray = null
81 | mainWindow = null
82 | })
83 | // 抖音过验证码弹窗
84 | let browserWindow
85 | // 接收renderer事件,抖音获取房间信息
86 | IpcMainUtil.on(IpcChannel.getDouyinRoomData, async (event, url) => {
87 | try {
88 | if (browserWindow && !browserWindow.destroyed()) {
89 | // 已有窗口打开, 销毁
90 | IpcMainUtil.sendToRenderer(mainWindow, IpcChannel.getDouyinRoomDataReply, url, false)
91 | browserWindow.destroy()
92 | browserWindow = null
93 | }
94 | let failed = true
95 | let hasSetShowWindowDelay = false
96 | browserWindow = new BrowserWindow({
97 | parent: mainWindow,
98 | height: 500,
99 | width: 800,
100 | show: false
101 | })
102 | browserWindow.loadURL(url)
103 | browserWindow.webContents.on('dom-ready', () => {
104 | // 页面刷新会触发
105 | browserWindow.webContents
106 | .executeJavaScript(
107 | `
108 | new Promise((resolve, reject) => {
109 | let data = false
110 | try {
111 | data = document.getElementById('RENDER_DATA').innerText;
112 | data = decodeURIComponent(data);
113 | console.log(data)
114 | } catch(e) {
115 | console.log('No roomInfo found')
116 | }
117 | resolve(data);
118 | });
119 | `, true).then((result) => {
120 | // console.log(result)
121 | if (!result) {
122 | if (!hasSetShowWindowDelay) {
123 | setTimeout(() => {
124 | // 未直接获取到房间信息,显示窗口
125 | if (failed && browserWindow && !browserWindow.destroyed()) {
126 | browserWindow.show()
127 | }
128 | }, 4000)
129 | hasSetShowWindowDelay = true
130 | }
131 | } else {
132 | IpcMainUtil.sendToRenderer(mainWindow, IpcChannel.getDouyinRoomDataReply, url, true, result)
133 | if (browserWindow) {
134 | browserWindow.destroy()
135 | browserWindow = null
136 | }
137 | failed = false
138 | }
139 | })
140 | })
141 | browserWindow.on('close', (closeEvent) => {
142 | closeEvent.preventDefault()
143 | browserWindow.webContents.executeJavaScript(`
144 | new Promise((resolve, reject) => {
145 | let data = false
146 | try {
147 | data = document.getElementById('RENDER_DATA').innerText;
148 | data = decodeURIComponent(data);
149 | console.log(data)
150 | } catch(e) {
151 | console.log('No roomInfo found')
152 | }
153 | resolve(data);
154 | });
155 | `, true).then((result) => {
156 | if (result) {
157 | IpcMainUtil.sendToRenderer(mainWindow, IpcChannel.getDouyinRoomDataReply, url, true, result)
158 | } else {
159 | IpcMainUtil.sendToRenderer(mainWindow, IpcChannel.getDouyinRoomDataReply, url, false)
160 | }
161 | if (browserWindow) {
162 | browserWindow.destroy()
163 | browserWindow = null
164 | }
165 | failed = false
166 | })
167 | })
168 | // 超时未响应关闭窗口,返回失败,6秒
169 | await new Promise(resolve => {
170 | setTimeout(() => {
171 | if (failed && browserWindow) {
172 | browserWindow.destroy()
173 | browserWindow = null
174 | }
175 | IpcMainUtil.sendToRenderer(mainWindow, IpcChannel.getDouyinRoomDataReply, url, false)
176 | resolve()
177 | }, 60000)
178 | })
179 | } catch (e) {
180 | IpcMainUtil.sendToRenderer(mainWindow, IpcChannel.getDouyinRoomDataReply, url, false)
181 | }
182 | })
183 | }
184 |
185 | function createTray () {
186 | tray = new Tray(path.join(__static, getTrayImageFileName()))
187 | let menu = Menu.buildFromTemplate([
188 | {
189 | label: '显示/隐藏 窗口',
190 | click: () => mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show()
191 | }
192 | ])
193 | tray.setContextMenu(menu)
194 | tray.setToolTip(getAppName())
195 | tray.on('click', () => mainWindow.show())
196 | }
197 |
198 | function getTrayImageFileName () {
199 | switch (process.platform) {
200 | case 'win32':
201 | return 'tray.ico'
202 | case 'darwin':
203 | case 'linux':
204 | default:
205 | return 'tray.png'
206 | }
207 | }
208 |
209 | function startGatewayExchangeServer () {
210 | // 内嵌服务支持修改UA等
211 | const expressServer = express()
212 | // 自定义跨域中间件
213 | const allowCors = function (req, res, next) {
214 | res.header('Access-Control-Allow-Origin', '*')
215 | res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS')
216 | res.header('Access-Control-Allow-Headers', '*')
217 | res.header('Access-Control-Allow-Credentials', 'true')
218 | next()
219 | }
220 | // 使用跨域中间件
221 | expressServer.use(allowCors)
222 | // 解析body
223 | expressServer.use(bodyParser.json())
224 | // 转发请求
225 | expressServer.post('/gateway/exchange', function (req, resp) {
226 | axios.request(req.body.config).then(res => {
227 | resp.status(200)
228 | resp.send(res.data)
229 | }).catch(e => {
230 | resp.status(500)
231 | resp.send(e.message)
232 | })
233 | })
234 | expressServer.listen(getGatewayExchangeServerPort(), 'localhost')
235 | }
236 |
237 | function init () {
238 | if (tray) {
239 | // 如果已存在托盘,销毁后再创建
240 | tray.destroy()
241 | }
242 | createTray()
243 | createWindow()
244 | startGatewayExchangeServer()
245 | }
246 | // 使用通知必填
247 | app.setAppUserModelId(getAppName())
248 |
249 | app.on('ready', init)
250 |
251 | app.on('window-all-closed', (e) => {
252 | if (tray) {
253 | // 销毁托盘,防止多次启动后关闭未及时销毁
254 | tray.destroy()
255 | }
256 | if (process.platform !== 'darwin') {
257 | app.quit()
258 | }
259 | })
260 |
261 | app.on('activate', () => {
262 | if (mainWindow === null) {
263 | createWindow()
264 | }
265 | })
266 |
267 | /**
268 | * Auto Updater
269 | *
270 | * Uncomment the following code below and install `electron-updater` to
271 | * support auto updating. Code Signing with a valid certificate is required.
272 | * https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-electron-builder.html#auto-updating
273 | */
274 |
275 | /*
276 | import { autoUpdater } from 'electron-updater'
277 |
278 | autoUpdater.on('update-downloaded', () => {
279 | autoUpdater.quitAndInstall()
280 | })
281 |
282 | app.on('ready', () => {
283 | if (process.env.NODE_ENV === 'production') autoUpdater.checkForUpdates()
284 | })
285 | */
286 |
--------------------------------------------------------------------------------
/src/renderer/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
117 |
118 |
138 |
--------------------------------------------------------------------------------
/src/renderer/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zbfzn/MultiPlatformLiveVideoRecorder/502d4757d026a06cebf5681387fcd7380e77c4b7/src/renderer/assets/.gitkeep
--------------------------------------------------------------------------------
/src/renderer/components/BasePage/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
42 |
43 |
77 |
--------------------------------------------------------------------------------
/src/renderer/components/CommonPage/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
10 |
11 |
12 |
13 |
38 |
39 |
50 |
--------------------------------------------------------------------------------
/src/renderer/components/ImageList/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 |
7 | {img.checked = !checked;onClick(index, img)}">
8 | {{ (index + 1)}}.
9 |
10 | {{img.title.substr(0, (showIndex ? 8 : 11)) + '...'}}
11 |
12 | {{img.title}}
13 |
14 |
15 |
16 |
17 |
18 |
19 |
104 |
105 |
141 |
--------------------------------------------------------------------------------
/src/renderer/components/Layout/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
22 |
23 |
52 |
--------------------------------------------------------------------------------
/src/renderer/components/Layout/components/AppMain.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
16 |
17 |
20 |
--------------------------------------------------------------------------------
/src/renderer/components/Layout/components/LeftMenus.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
13 |
14 |
15 |
75 |
76 |
82 |
--------------------------------------------------------------------------------
/src/renderer/components/SLink/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{text}}
5 |
6 |
7 |
8 |
33 |
34 |
43 |
--------------------------------------------------------------------------------
/src/renderer/components/SubPage/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
68 |
69 |
83 |
--------------------------------------------------------------------------------
/src/renderer/components/Views/About/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{$route.meta.title}}
4 |
5 |
6 |
10 | ZuoBro
11 |
12 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
34 |
35 |
38 |
--------------------------------------------------------------------------------
/src/renderer/components/Views/Record/Add.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
41 |
42 |
51 |
--------------------------------------------------------------------------------
/src/renderer/components/Views/Record/Edit.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
39 |
40 |
43 |
--------------------------------------------------------------------------------
/src/renderer/components/Views/Settings/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{$route.meta.title}}
4 |
5 |
6 |
9 |
10 |
11 |
12 |
15 |
16 |
17 | 选取
18 |
19 |
20 |
21 |
22 | 输出文件夹应用到当前所有录制任务
23 |
24 |
25 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
38 |
39 |
40 |
41 |
42 |
43 |
45 |
48 |
49 |
50 |
51 |
52 |
53 | 保存
54 |
55 |
56 |
57 |
58 |
59 |
187 |
188 |
200 |
--------------------------------------------------------------------------------
/src/renderer/components/Views/VideoDownload/DownloadManager.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 下载中
7 |
8 |
9 |
10 | 等待队列
11 |
12 |
13 |
14 | 下载记录
15 |
16 |
17 |
18 |
19 |
20 |
21 |
34 |
35 |
36 |
37 |
38 |
39 |
109 |
110 |
116 |
--------------------------------------------------------------------------------
/src/renderer/components/Views/VideoDownload/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ $route.meta.title }}
4 |
5 |
event.keyCode === 13 && parseUrlForVideos()" clearable>
6 |
8 |
9 |
10 |
11 |
12 |
13 | 视频数:{{ imgList.length }}
14 | 取消全选
15 | 全选
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | {{ (preDownloadDialogData.adapter && preDownloadDialogData.adapter.info ? preDownloadDialogData.adapter.info.name : '') }}
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | {/*先清除上次选择的目录*/$refs.folderSelect.value = null;$refs.folderSelect.click()}">选取
46 |
47 |
48 |
49 | 取消
50 | 加入下载队列
51 |
52 |
53 |
54 |
55 |
56 | 下载已选择
57 |
58 | 下载管理
59 |
60 |
61 |
62 |
63 |
218 |
219 |
235 |
--------------------------------------------------------------------------------
/src/renderer/components/Views/VideoDownload/ccomponents/DownloadHistoryView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | event.keyCode === 13 && search()">
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | 搜索
14 | 刷新
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | {{scope.row.title}}
25 |
26 |
27 |
28 |
29 | {{scope.row.adapter ? scope.row.adapter.name : ''}}
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | 解析中..
40 | 下载中..
41 | 下载完成
42 |
43 | 下载错误
44 |
45 | 已取消
46 | 意外终止
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
63 |
64 |
65 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
282 |
283 |
286 |
--------------------------------------------------------------------------------
/src/renderer/components/Views/VideoDownload/ccomponents/DownloadWaitingView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | {{scope.row.title}}
12 |
13 |
14 |
15 |
16 | {{scope.row.adapter ? scope.row.adapter.name : ''}}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | 解析中..
27 | 下载中..
28 | 下载完成
29 | 下载错误
30 | 已取消
31 | 意外终止
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
106 |
107 |
110 |
--------------------------------------------------------------------------------
/src/renderer/components/Views/VideoDownload/ccomponents/DownloadingView.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | {{scope.row.title}}
12 |
13 |
14 |
15 |
16 | {{scope.row.adapter ? scope.row.adapter.name : ''}}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | 解析中..
27 | 下载中..
28 | 下载完成
29 | 下载错误
30 | 已取消
31 | 意外终止
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
197 |
198 |
201 |
--------------------------------------------------------------------------------
/src/renderer/config/SysNotice.js:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 |
3 | const SysNotice = {}
4 |
5 | /**
6 | * 创建系统通知
7 | * @param title 标题
8 | * @param body 内容
9 | * @return {Notification}
10 | */
11 | export function createNotice (title, body) {
12 | return new Notification(title, {
13 | icon:
14 | process.platform === 'win32'
15 | ? path.join(__static, 'icon.ico')
16 | : null,
17 | body
18 | })
19 | }
20 |
21 | SysNotice.createNotice = createNotice
22 | export default SysNotice
23 |
--------------------------------------------------------------------------------
/src/renderer/config/log.js:
--------------------------------------------------------------------------------
1 | function getStringMessage (message) {
2 | if (!message) {
3 | return ''
4 | } else if (message instanceof Object) {
5 | return message.toString()
6 | } else if (message instanceof Array) {
7 | return message.join(',')
8 | } else {
9 | return message
10 | }
11 | }
12 | const LogPrefix = 'LVR'
13 | const Log = {
14 | debug (message) {
15 | console.log(LogPrefix + '-debug: ' + getStringMessage(message))
16 | },
17 | info (message) {
18 | console.log(LogPrefix + '-info: ' + getStringMessage(message))
19 | },
20 | warn (message) {
21 | console.log(LogPrefix + '-warn: ' + getStringMessage(message))
22 | },
23 | error (message) {
24 | console.log(LogPrefix + '-error: ' + getStringMessage(message))
25 | }
26 | }
27 | export default Log
28 |
--------------------------------------------------------------------------------
/src/renderer/config/settings.js:
--------------------------------------------------------------------------------
1 | import dbs from '../db'
2 | const Settings = {}
3 |
4 | /**
5 | * 是否开启自动检查
6 | */
7 | export function enableAutoCheck () {
8 | return new Promise((resolve, reject) => {
9 | dbs.settings.$find({}).sort({updateTime: -1}).then((e, docs) => {
10 | if (e) {
11 | reject(e)
12 | } else {
13 | resolve(docs.length > 0 ? docs[0].enableAutoCheck : null)
14 | }
15 | })
16 | })
17 | }
18 |
19 | export function getAppOutPutPath () {
20 | return new Promise((resolve, reject) => {
21 | dbs.settings.$find({}).sort({updateTime: -1}).then((e, docs) => {
22 | if (e) {
23 | reject(e)
24 | } else {
25 | resolve(docs.length > 0 ? docs[0].defaultOuPutDir : null)
26 | }
27 | })
28 | })
29 | }
30 |
31 | export function getSettings () {
32 | return new Promise((resolve, reject) => {
33 | dbs.settings.$find({}).sort({updateTime: -1}).then((e, docs) => {
34 | if (e) {
35 | reject(e)
36 | } else {
37 | resolve(docs.length > 0 ? docs[0] : null)
38 | }
39 | })
40 | })
41 | }
42 |
43 | /**
44 | * 保存设置,不删除旧设置
45 | * @param settings
46 | * @return {Promise}
47 | */
48 | export function saveSettings (settings) {
49 | settings.updateTime = new Date()
50 | return dbs.settings._insert_(settings)
51 | }
52 |
53 | export default Settings
54 |
--------------------------------------------------------------------------------
/src/renderer/db/index.js:
--------------------------------------------------------------------------------
1 | import Datastore from 'nedb'
2 | import path from 'path'
3 | import { remote } from 'electron'
4 | import fs from 'fs'
5 | import readline from 'readline'
6 |
7 | // 分页查询
8 | /**
9 | * @param query {Object}
10 | * @return {any}
11 | */
12 | Datastore.prototype.$find = function (query) {
13 | let cursor = this.find(query)
14 | let _cursor = {
15 | /**
16 | * @param num {Number} 最大条数
17 | * @return {any}
18 | */
19 | limit: num => {
20 | cursor.limit(num)
21 | return _cursor
22 | },
23 | /**
24 | * @param num {Number}舍弃条数
25 | * @return {any}
26 | */
27 | skip: num => {
28 | cursor.skip(num)
29 | return _cursor
30 | },
31 | /**
32 | * 排序
33 | * @param sortQuery
34 | */
35 | sort: sortQuery => {
36 | cursor.sort(sortQuery)
37 | return _cursor
38 | },
39 | then: callback => {
40 | cursor.exec(callback)
41 | }
42 | }
43 | return _cursor
44 | }
45 |
46 | Datastore.prototype.$find_ = function (query) {
47 | let cursor = this.find(query)
48 | let _cursor = {
49 | /**
50 | * @param num {Number} 最大条数
51 | * @return {any}
52 | */
53 | limit: num => {
54 | cursor.limit(num)
55 | return _cursor
56 | },
57 | /**
58 | * @param num {Number}舍弃条数
59 | * @return {any}
60 | */
61 | skip: num => {
62 | cursor.skip(num)
63 | return _cursor
64 | },
65 | /**
66 | * 排序
67 | * @param sortQuery
68 | */
69 | sort: sortQuery => {
70 | cursor.sort(sortQuery)
71 | return _cursor
72 | },
73 | execute () {
74 | return new Promise((resolve, reject) => {
75 | cursor.exec((e, docs) => {
76 | if (!e) {
77 | resolve(docs)
78 | } else {
79 | reject(e)
80 | }
81 | })
82 | })
83 | }
84 | }
85 | return _cursor
86 | }
87 |
88 | Datastore.prototype._find_ = function (query) {
89 | return new Promise((resolve, reject) => {
90 | this.find(query, (e, docs) => {
91 | if (e) {
92 | reject(e)
93 | } else {
94 | resolve(docs)
95 | }
96 | })
97 | })
98 | }
99 | Datastore.prototype._update_ = function (query, updated, rejection) {
100 | return new Promise((resolve, reject) => {
101 | this.update(query, updated, rejection, (e, num) => {
102 | if (e) {
103 | reject(e)
104 | } else {
105 | resolve(num)
106 | }
107 | })
108 | })
109 | }
110 | Datastore.prototype._count_ = function (query) {
111 | return new Promise((resolve, reject) => {
112 | this.count(query, (e, count) => {
113 | if (e) {
114 | reject(e)
115 | } else {
116 | resolve(count)
117 | }
118 | })
119 | })
120 | }
121 | Datastore.prototype._insert_ = function (doc) {
122 | return new Promise((resolve, reject) => {
123 | this.insert(doc, (e, ndoc) => {
124 | if (e) {
125 | reject(e)
126 | } else {
127 | resolve(ndoc)
128 | }
129 | })
130 | })
131 | }
132 | Datastore.prototype._remove_ = function (query, rejection) {
133 | return new Promise((resolve, reject) => {
134 | if (!rejection) {
135 | rejection = {}
136 | }
137 | this.remove(query, rejection, (e, num) => {
138 | if (e) {
139 | reject(e)
140 | } else {
141 | resolve(num)
142 | }
143 | })
144 | })
145 | }
146 | // 录制信息
147 | const record = new Datastore({
148 | autoload: false,
149 | filename: path.join(remote.app.getPath('userData'), '/db/Record.db')
150 | })
151 | // 录制历史
152 | const recordHistory = new Datastore({
153 | autoload: false,
154 | filename: path.join(remote.app.getPath('userData'), '/db/RecordHistory.db')
155 | })
156 | // 设置
157 | const settings = new Datastore({
158 | autoload: false,
159 | filename: path.join(remote.app.getPath('userData'), '/db/Settings.db')
160 | })
161 |
162 | // 下载记录
163 | const downloadRecord = new Datastore({
164 | autoload: false,
165 | filename: path.join(remote.app.getPath('userData'), '/db/DownloadRecord.db')
166 | })
167 | console.log(record)
168 | console.log(recordHistory)
169 | console.log(settings)
170 | console.log(downloadRecord)
171 |
172 | export async function checkAndRebuild (dbs, onError, onStart, onFinished, onFinally) {
173 | const errorCallback = onError && typeof onError === 'function' ? onError : _ => {}
174 | var startFunReturn
175 | const rebuildList = []
176 | try {
177 | for (let db of Object.values(dbs)) {
178 | // 判断单个db文件是否超256MB
179 | if (fs.statSync(db.filename).size / (1024 * 1024) > 256) {
180 | // 放入重建列表
181 | rebuildList.push(db)
182 | } else {
183 | // 数据库文件正常则加载数据库
184 | db.loadDatabase()
185 | }
186 | }
187 | if (rebuildList.length > 0) {
188 | if (onStart && typeof onStart === 'function') {
189 | startFunReturn = onStart()
190 | }
191 | const promises = []
192 | // 重建
193 | for (const db of rebuildList) {
194 | promises.push(new Promise((resolve, reject) => {
195 | const rebFileName = db.filename + '.reb'
196 | const blockedDbFileName = db.filename + '.blocked'
197 | const recordMap = {}
198 | const rl = readline.createInterface({
199 | input: fs.createReadStream(db.filename, {
200 | encoding: 'utf-8'
201 | }),
202 | output: process.stdout,
203 | terminal: false
204 | })
205 | rl.on('line', line => {
206 | if (!line || line.trim().length === 0) {
207 | return
208 | }
209 | const rec = JSON.parse(line)
210 | if (rec['$$deleted']) {
211 | // 已删除
212 | delete recordMap[rec._id]
213 | return
214 | }
215 | // 保留最后一条记录
216 | recordMap[rec._id] = rec
217 | })
218 | rl.on('close', _ => {
219 | try {
220 | fs.writeFileSync(rebFileName, Object.values(recordMap).map(item => JSON.stringify(item)).join('\n'))
221 | // 当前文件备份
222 | fs.renameSync(db.filename, blockedDbFileName)
223 | // 重建的数据库文件替换
224 | fs.renameSync(rebFileName, db.filename)
225 | // 重载
226 | db.loadDatabase()
227 | // 加载数据库成功,删除异常文件
228 | fs.unlinkSync(blockedDbFileName)
229 | } catch (e) {
230 | reject(e)
231 | }
232 | resolve()
233 | })
234 | }))
235 | }
236 | await Promise.all(promises).then(_ => {
237 | if (onFinished && typeof onFinished === 'function') {
238 | onFinished(startFunReturn)
239 | }
240 | }).catch(e => {
241 | throw e
242 | })
243 | }
244 | } catch (e) {
245 | errorCallback(e)
246 | if (onFinished && typeof onFinished === 'function') {
247 | onFinished(startFunReturn)
248 | }
249 | } finally {
250 | if (onFinally && typeof onFinally === 'function') {
251 | onFinally(startFunReturn)
252 | }
253 | }
254 | }
255 |
256 | export default {
257 | record,
258 | recordHistory,
259 | settings,
260 | downloadRecord
261 | }
262 |
--------------------------------------------------------------------------------
/src/renderer/global.js:
--------------------------------------------------------------------------------
1 | import ffmpeg from 'fluent-ffmpeg'
2 | import ffmpegStatic from 'ffmpeg-static'
3 | import fs from 'fs'
4 | import path from 'path'
5 |
6 | const Global = {
7 | init: () => {
8 | // eslint-disable-next-line no-extend-native
9 | Date.prototype.Format = function (fmt) {
10 | let o = {
11 | 'M+': this.getMonth() + 1,
12 | 'd+': this.getDate(),
13 | 'H+': this.getHours(),
14 | 'm+': this.getMinutes(),
15 | 's+': this.getSeconds(),
16 | 'S+': this.getMilliseconds()
17 | }
18 | if (/(y+)/.test(fmt)) {
19 | fmt = fmt.replace(RegExp.$1, (this.getFullYear() + '').substr(4 - RegExp.$1.length))
20 | }
21 | for (let k in o) {
22 | if (new RegExp('(' + k + ')').test(fmt)) {
23 | fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (('00' + o[k]).substr(String(o[k]).length)))
24 | }
25 | }
26 | return fmt
27 | }
28 |
29 | console.log(ffmpegStatic)
30 | // ffmpeg-static node_moudles 删除其他平台二进制文件或修改package.json
31 | ffmpeg.setFfmpegPath(ffmpegStatic.path.replace('app.asar', 'app.asar.unpacked'))
32 |
33 | // 递归创建目录 同步方法
34 | fs.mkdirsSync = function (dirname) {
35 | if (fs.existsSync(dirname)) {
36 | return true
37 | } else {
38 | if (this.mkdirsSync(path.dirname(dirname))) {
39 | fs.mkdirSync(dirname)
40 | return true
41 | }
42 | }
43 | }
44 | // linkedom 解决找不到globalThis问题, 修改node_modules/linkedom/node_modules/htmlparser2/lib/esm/index.js,parseFeed替换成下面导出方式
45 | /*
46 | // 替换函数
47 | const parseFeed = function (feed, options = { xmlMode: true }) {
48 | return getFeed(parseDOM(feed, options));
49 | }
50 | export parseFeed;
51 | */
52 | // eslint-disable-next-line no-undef
53 | global.globalThis = {}
54 | }
55 | }
56 | export default Global
57 |
--------------------------------------------------------------------------------
/src/renderer/live-platform/BilibiliLivePlatform.js:
--------------------------------------------------------------------------------
1 | import LivePlatform from './live-platform.js'
2 | import HttpUtil from '../utils/http-util.js'
3 | import qs from 'query-string'
4 |
5 | class BilibiliLivePlatform extends LivePlatform {
6 | constructor () {
7 | super()
8 | this.httpHeaders = {
9 | 'User-Agent': this.pcUserAgent,
10 | 'referer': 'https://live.bilibili.com'
11 | }
12 | }
13 | getUserInfo (idOrUrl) {
14 | return new Promise(async (resolve, reject) => {
15 | let roomId = idOrUrl
16 | if (this.isUrl(idOrUrl)) {
17 | roomId = new RegExp('https?://live.bilibili.com/(\\d+)').exec(idOrUrl)[1]
18 | }
19 | try {
20 | const roomInitUrl = `https://api.live.bilibili.com/room/v1/Room/room_init?id=${roomId}`
21 | let roomInitInfoRes = await HttpUtil.get(roomInitUrl, this.httpHeaders)
22 | // 获取真实房间号
23 | if (roomInitInfoRes.data.msg === '直播间不存在') {
24 | reject(new Error('直播间不存在'))
25 | return
26 | }
27 | const realRoomId = roomInitInfoRes.data.data.room_id
28 | const roomInfoUrl = `https://api.live.bilibili.com/xlive/web-room/v1/index/getInfoByRoom?room_id=${realRoomId}`
29 | let roomInfoRes = await HttpUtil.get(roomInfoUrl, this.httpHeaders)
30 | const userName = roomInfoRes.data.data.anchor_info.base_info.uname
31 | resolve({
32 | id: roomId,
33 | idShow: roomId,
34 | name: userName,
35 | roomId: roomId
36 | })
37 | } catch (e) {
38 | reject(e)
39 | }
40 | })
41 | }
42 | check (user, preferenceQuality, preferenceChannel) {
43 | // 调用方法前保证idOrUrl是id
44 | return new Promise(async (resolve, reject) => {
45 | try {
46 | const roomInitUrl = `https://api.live.bilibili.com/room/v1/Room/room_init?id=${user.id}`
47 | let roomInitInfoRes = await HttpUtil.get(roomInitUrl, this.httpHeaders)
48 | // 获取真实房间号
49 | if (roomInitInfoRes.data.msg === '直播间不存在') {
50 | reject(new Error('直播间不存在'))
51 | return
52 | } else if (roomInitInfoRes.data.data.live_status !== 1) {
53 | // 未开播
54 | resolve()
55 | return
56 | }
57 | const realRoomId = roomInitInfoRes.data.data.room_id
58 | const roomInfoUrl = `https://api.live.bilibili.com/xlive/web-room/v1/index/getInfoByRoom?room_id=${realRoomId}`
59 | let roomInfoRes = await HttpUtil.get(roomInfoUrl, this.httpHeaders)
60 | const title = roomInfoRes.data.data.room_info.title
61 | // 获取直播流数据
62 | const usableQualityValues = await this.getUsableQualities(realRoomId)
63 | // 最优清晰度(没有设置的最高清晰度依次向下类推)
64 | const realQuality = LivePlatform.getPreference('value', 'orderIndex', BilibiliLivePlatform.defaultConfig.qualities, usableQualityValues, preferenceQuality.value)
65 | if (!realQuality) {
66 | reject(new Error('无可用清晰度'))
67 | }
68 | const realChannel = preferenceChannel
69 | const streamUrl = await this.getStreamUrl(realRoomId, realQuality.value)
70 | // 使用浏览器头会403
71 | const result = {
72 | streamUrl: streamUrl,
73 | ffmpegCommandOutputOption: {
74 | '-headers': `User-Agent: PostmanRuntime/7.28.4`,
75 | '-i': streamUrl,
76 | '-v': 'trace',
77 | '-c': 'copy',
78 | '-flvflags': 'add_keyframe_index'
79 | },
80 | title: title,
81 | quality: realQuality,
82 | channel: realChannel
83 | }
84 | resolve(result)
85 | console.log(result)
86 | } catch (e) {
87 | reject(e)
88 | }
89 | })
90 | }
91 |
92 | // ********************************重写方法分割**************************** //
93 | getUsableQualities (realRoomId) {
94 | return new Promise(async (resolve, reject) => {
95 | try {
96 | const param = qs.stringify({
97 | 'room_id': realRoomId,
98 | 'protocol': '0,1',
99 | 'format': '0,1,2',
100 | 'codec': '0,1',
101 | 'qn': 10000,
102 | 'platform': 'h5',
103 | 'ptype': 8
104 | })
105 | let res = await HttpUtil.get('https://api.live.bilibili.com/xlive/web-room/v2/index/getRoomPlayInfo?' + param, this.httpHeaders)
106 | let streamInfo = res.data.data.playurl_info.playurl.stream
107 | let stream = null
108 | for (const sm of streamInfo) {
109 | if (sm.protocol_name === 'http_stream') {
110 | stream = sm
111 | break
112 | }
113 | }
114 | if (!stream) {
115 | reject(new Error('获取直播流信息失败'))
116 | return
117 | }
118 | resolve(stream.format[0].codec[0].accept_qn)
119 | } catch (e) {
120 | reject(e)
121 | }
122 | })
123 | }
124 | getStreamUrl (realRoomId, qn) {
125 | return new Promise(async (resolve, reject) => {
126 | try {
127 | const param = qs.stringify({
128 | 'room_id': realRoomId,
129 | 'protocol': '0,1',
130 | 'format': '0,1,2',
131 | 'codec': '0,1',
132 | 'qn': qn,
133 | 'platform': 'h5',
134 | 'ptype': 8
135 | })
136 | let res = await HttpUtil.get('https://api.live.bilibili.com/xlive/web-room/v2/index/getRoomPlayInfo?' + param, this.httpHeaders)
137 | let streamInfo = res.data.data.playurl_info.playurl.stream
138 | let stream = null
139 | for (const sm of streamInfo) {
140 | if (sm.protocol_name === 'http_stream') {
141 | stream = sm
142 | break
143 | }
144 | }
145 | if (!stream) {
146 | reject(new Error('获取直播流信息失败'))
147 | return
148 | }
149 | const baseUrl = stream.format[0].codec[0].base_url
150 | const urlInfo = stream.format[0].codec[0].url_info[0]
151 | resolve(`${urlInfo.host}${baseUrl}${urlInfo.extra}`)
152 | } catch (e) {
153 | reject(e)
154 | }
155 | })
156 | }
157 | }
158 | /**
159 | * 获取用户房间地址或主页地址
160 | * @param user 用户信息
161 | */
162 | BilibiliLivePlatform.getUserRoomOrHomeUrl = function (user) {
163 | return 'https://live.bilibili.com/' + user.id
164 | }
165 |
166 | /**
167 | * 检查是否支持
168 | **/
169 | BilibiliLivePlatform.isSupportedUrl = function (url) {
170 | return /https?:\/\/live\.bilibili\.com\/.*/.test(url)
171 | }
172 |
173 | // qn=150高清 qn=250超清 qn=400蓝光 qn=10000原画
174 | BilibiliLivePlatform.platformInfo = {
175 | code: 'bilibii',
176 | name: '哔哩哔哩',
177 | website: 'https://live.bilibili.com'
178 | }
179 | BilibiliLivePlatform.defaultConfig = {
180 | qualities: [
181 | {
182 | code: 'GQ',
183 | name: '高清',
184 | value: 150,
185 | orderIndex: 1
186 | },
187 | {
188 | code: 'CQ',
189 | name: '超清',
190 | value: 250,
191 | orderIndex: 2
192 | },
193 | {
194 | code: 'LG',
195 | name: '蓝光',
196 | value: 400,
197 | orderIndex: 3
198 | },
199 | {
200 | code: 'YH',
201 | name: '原画',
202 | value: 10000,
203 | orderIndex: 4
204 | }
205 | ],
206 | channels: [
207 | {
208 | code: 'DEFAULT',
209 | name: '线路1',
210 | value: null,
211 | orderIndex: 1
212 | }
213 | ],
214 | defaultQualityCode: 'YH',
215 | defaultChannelCode: 'DEFAULT',
216 | idPlaceholder: '请输入直播间地址或房间号',
217 | dynamicRoomId: false
218 | }
219 | export default BilibiliLivePlatform
220 |
--------------------------------------------------------------------------------
/src/renderer/live-platform/DouyinLivePlatform.js:
--------------------------------------------------------------------------------
1 | import LivePlatform from './live-platform.js'
2 | import HttpUtil from '../utils/http-util.js'
3 | // 借助electron 窗口管理,打开一个隐藏窗口获取抖音房间信息,跳过反扒,后续可以分析反扒代码替换掉
4 | import IpcChannel from '../../helper/IpcChannel'
5 | import ipcRendererUtil from '../../helper/ipcRendererUtil'
6 |
7 | function getRoomDataFromWindow (liveUrl) {
8 | return new Promise((resolve, reject) => {
9 | // 通知主进程启动新窗口获取房间信息
10 | const listener = (event, url_, success, data) => {
11 | if (liveUrl === url_) {
12 | if (success) {
13 | resolve(data)
14 | } else {
15 | reject(new Error('获取直播流信息失败'))
16 | }
17 | ipcRendererUtil.removeListener(IpcChannel.getDouyinRoomDataReply, listener)
18 | }
19 | }
20 | ipcRendererUtil.on(IpcChannel.getDouyinRoomDataReply, listener)
21 | ipcRendererUtil.sendToMain(IpcChannel.getDouyinRoomData, liveUrl)
22 | })
23 | }
24 |
25 | class DouyinLivePlatform extends LivePlatform {
26 | constructor () {
27 | super()
28 | this.httpHeaders = {
29 | 'Content-Type': 'application/x-www-from-urlencoded',
30 | 'User-Agent': this.pcUserAgent
31 | }
32 | }
33 | getUserInfo (idOrUrl) {
34 | return new Promise(async (resolve, reject) => {
35 | try {
36 | let url = idOrUrl
37 | if (!this.isUrl(idOrUrl)) {
38 | url = `https://www.douyin.com/user/${idOrUrl}?previous_page=app_code_link`
39 | }
40 | let id = null
41 | try {
42 | id = new RegExp('(?:/user/|sec_uid=)([^/&?]*)(?:$|\\?|&)', 'g').exec(idOrUrl)[1]
43 | } catch (e) {
44 | //
45 | }
46 | if (!id) {
47 | let res = await HttpUtil.post(url, this.httpHeaders)
48 | // 获取sec_uid
49 | id = new RegExp('(?:/user/|sec_uid=)([^/&?]*)(?:$|\\?|&)', 'g').exec(res.request.responseURL)[1]
50 | }
51 | let resReal = await HttpUtil.post(`https://www.douyin.com/user/${id}?previous_page=app_code_link`, null, this.httpHeaders)
52 | // 获取用户页,获取用户名
53 | let userName = new RegExp('(?:){4}([^<>]+)(?:){4}', 'g').exec(resReal.data)[1]
54 | let douId = new RegExp(/]*>抖音号:(?:)?([^<>]+)<\/span>/g).exec(resReal.data)[1]
55 | let user = {
56 | id: id,
57 | idShow: douId,
58 | name: userName,
59 | roomId: null
60 | }
61 | console.log(user)
62 | resolve(user)
63 | } catch (e) {
64 | reject(e)
65 | }
66 | })
67 | }
68 | check (user, preferenceQuality, preferenceChannel) {
69 | return new Promise(async (resolve, reject) => {
70 | try {
71 | let roomInfo
72 | // 抖音关闭手机端web直播页,故放弃
73 | // if (user.roomId) {
74 | // const exchangeData = {
75 | // config: {
76 | // url: `https://webcast.amemv.com/webcast/reflow/${user.roomId}`,
77 | // method: 'get',
78 | // headers: {
79 | // 'User-Agent': this.phoneUserAgent
80 | // }
81 | // }
82 | // }
83 | // // 手机端web页获取,调用转发服务获取手机端页面数据
84 | // const res = await HttpUtil.postExchange(exchangeData)
85 | // const re = new RegExp('window.__INIT_PROPS__ = ([^<]*)')
86 | // const find = re.exec(res.data)
87 | // roomInfo = JSON.parse(find[1])['/webcast/reflow/:id']['room']
88 | // if (roomInfo.status !== 2 && roomInfo.status !== 4) {
89 | // resolve()
90 | // return
91 | // } else if (roomInfo.status === 4) {
92 | // // 已开播,但直播间房间号已改变
93 | // roomInfo = null
94 | // }
95 | // }
96 | if (!roomInfo) {
97 | // 没有app端房间id,使用web端直播页获取(可能需要验证码验证)
98 | let webRid = null
99 | // 采用post(不是get就行)避开抖音安全校验, 获取用户房间信息
100 | const res = await HttpUtil.post(`https://www.douyin.com/user/${user.id}?previous_page=app_code_link`, null, this.httpHeaders)
101 | const re = new RegExp('').exec(html)[1])
18 | const videoData = initData.videoData
19 | const videoInfoApi = `https://v-api-player-ssl.huya.com/?r=vhuyaplay%2Fvideo&vid=${videoData.vid}&format=mp4%2Cm3u8&_=${new Date().getTime()}`
20 | const videoInfo = (await httpUtil.get(videoInfoApi, this.httpHeaders)).data
21 | // 选择最佳清晰度
22 | preferenceQuality = this.getQuality(preferenceQuality.code)
23 | const quality = VideoDownloadAdapter.getPreference('value', 'orderIndex', HuyaVideoDownloadAdapter.defaultConfig.qualities, videoInfo.result.items.map(item => item.definition), preferenceQuality.value)
24 | if (!quality) {
25 | reject(new Error('无可用清晰度'))
26 | return
27 | }
28 | for (let item of videoInfo.result.items) {
29 | if (quality.value === item.definition) {
30 | const flvUrl = item.transcode.urls[0]
31 | resolve({
32 | streamUrls: flvUrl,
33 | title: videoData.videoTitle,
34 | owner: videoData.userInfo.userNick,
35 | quality: quality,
36 | ffmpegCommandOutputOption: {
37 | '-headers': `Referer: ${url}`,
38 | '-user_agent': this.pcUserAgent,
39 | '-i': flvUrl,
40 | '-c': 'copy'
41 | }
42 | })
43 | break
44 | }
45 | }
46 | reject(new Error('未匹配到对应清晰度!'))
47 | } catch (e) {
48 | reject(e)
49 | }
50 | })
51 | }
52 |
53 | async getVideoDetails (url) {
54 | // 获取页面数据,返回所有数据,页面进行分页
55 | return new Promise(async (resolve, reject) => {
56 | // 列表解析
57 | const videoDetailList = []
58 | try {
59 | if (new RegExp('https?://v\\.huya\\.com/play/(\\d+)\\.html').test(url)) {
60 | // 单个视频
61 | const html = (await httpUtil.get(url, this.httpHeaders)).data
62 | const initData = JSON.parse(new RegExp('window.HNF_GLOBAL_INIT\\s?=\\s?([^<]+)').exec(html)[1])
63 | const videoData = initData.videoData
64 | resolve([
65 | {
66 | id: videoData.vid,
67 | title: videoData.videoTitle,
68 | cover: this.wrapImageUrl(videoData.covers.cover),
69 | url: url
70 | }
71 | ])
72 | } else if (new RegExp('https?://v\\.huya\\.com/u/(\\d+)(?:/(video|livevideo)\\.html)?').test(url)) {
73 | const uid = new RegExp('https?://v\\.huya\\.com/u/(\\d+)(?:/(video|livevideo)\\.html)?').exec(url)[1]
74 | let videoType = 'video'
75 | if (url.indexOf('video.html') > -1) {
76 | videoType = new RegExp('https?://v\\.huya\\.com/u/(\\d+)/(video|livevideo)\\.html').exec(url)[2]
77 | }
78 | let cPage = 0
79 | let totalPages = -1
80 | do {
81 | cPage += 1
82 | const pageUrl = `https://v.huya.com/u/${uid}/${videoType}.html?sort=news&p=${cPage}`
83 | const html = (await httpUtil.get(pageUrl, this.httpHeaders)).data
84 | const document = parseHTML(html).window.document
85 | // 获取当前页列表
86 | for (let li of document.querySelectorAll(`.${videoType === 'video' ? 'content-list' : 'section-list'} li`)) {
87 | const a = li.querySelectorAll('a')[0]
88 | // 视频信息
89 | videoDetailList.push({
90 | id: this.getVideoId(a.href),
91 | title: a.title,
92 | cover: this.wrapImageUrl(a.querySelectorAll('img')[0].src),
93 | url: `https://v.huya.com${a.href}`
94 | })
95 | }
96 | // 获取总页数
97 | const pageAs = document.querySelectorAll('.user-paginator a.paginator-page')
98 | totalPages = Number(pageAs[pageAs.length - 1].innerText)
99 | } while (cPage < totalPages)
100 | resolve(videoDetailList)
101 | } else {
102 | reject(new Error('暂不支持该链接!'))
103 | }
104 | } catch (e) {
105 | reject(e)
106 | }
107 | })
108 | }
109 |
110 | getQuality (code) {
111 | for (let quality of HuyaVideoDownloadAdapter.defaultConfig.qualities) {
112 | if (code === quality.code) {
113 | return quality
114 | }
115 | }
116 | return null
117 | }
118 | getQualityByValue (value) {
119 | for (let quality of HuyaVideoDownloadAdapter.defaultConfig.qualities) {
120 | if (value === quality.value) {
121 | return quality
122 | }
123 | }
124 | return null
125 | }
126 | getVideoId (urlPath) {
127 | return /\/play\/(\d+)\.html/.exec(urlPath)[1]
128 | }
129 | wrapImageUrl (url) {
130 | return url.indexOf('http') === 0 ? url : ('https:' + url)
131 | }
132 | }
133 | HuyaVideoDownloadAdapter.info = {
134 | code: 'huya',
135 | name: '虎牙',
136 | webUrl: 'https://v.huya.com'
137 | }
138 |
139 | HuyaVideoDownloadAdapter.defaultConfig = {
140 | qualities: [
141 | {
142 | code: 'LC',
143 | name: '流畅',
144 | value: '350',
145 | orderIndex: 1
146 | },
147 | {
148 | code: 'GQ',
149 | name: '高清',
150 | value: '1000',
151 | orderIndex: 2
152 | },
153 | {
154 | code: 'CQ',
155 | name: '超清',
156 | value: '1300',
157 | orderIndex: 3
158 | },
159 | {
160 | code: 'YH',
161 | name: '原画',
162 | value: 'yuanhua',
163 | orderIndex: 4
164 | }
165 | ]
166 | }
167 | export default HuyaVideoDownloadAdapter
168 |
--------------------------------------------------------------------------------
/src/renderer/video-download-adapter/VideoDownloadAdapter.js:
--------------------------------------------------------------------------------
1 | class VideoDownloadAdapter {
2 | constructor () {
3 | this.phoneUserAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1'
4 | this.pcUserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36'
5 | }
6 | async getDownloadInfo (url, preferenceQualityCode) {
7 | throw Error('method not implemented')
8 | }
9 | async getVideoDetails (url) {
10 | throw Error('method not implemented')
11 | }
12 | }
13 | // 输出格式
14 | VideoDownloadAdapter.supportVideoOutPutFormats = {
15 | 'flv': {
16 | '-flvflags': 'add_keyframe_index'
17 | },
18 | 'mp4': {
19 | '-f': 'mp4'
20 | },
21 | 'mov': {
22 | '-f': 'mov'
23 | },
24 | 'mkv': {},
25 | 'avi': {
26 | '-bsf:v': 'h264_mp4toannexb'
27 | },
28 | 'ts': {}
29 | }
30 |
31 | VideoDownloadAdapter.setFFmpegOutputOptions = (ffmpeg_, ffmpegOutputOption) => {
32 | if (ffmpegOutputOption instanceof Array) {
33 | // 支持追加相同key内容
34 | for (let index in ffmpegOutputOption) {
35 | for (const key of Object.keys(ffmpegOutputOption[index])) {
36 | ffmpeg_.outputOptions(key, ffmpegOutputOption[index][key])
37 | }
38 | }
39 | } else {
40 | for (const key of Object.keys(ffmpegOutputOption)) {
41 | ffmpeg_.outputOptions(key, ffmpegOutputOption[key])
42 | }
43 | }
44 | }
45 |
46 | /**
47 | * 寻找最佳匹配
48 | * @param findKey 查找key
49 | * @param orderKey 排序key
50 | * @param findArray 所有匹配
51 | * @param filterArray 可用匹配
52 | * @param value 查找值
53 | * @return {null|*}
54 | */
55 | VideoDownloadAdapter.getPreference = (findKey, orderKey, findArray, filterArray, value) => {
56 | findArray = findArray.sort((a, b) => {
57 | // 根据orderKey的值降序, orderIndex越大越优
58 | return a[orderKey] > b[orderKey] ? -1 : 1
59 | })
60 | let filterOb = {}
61 | for (let findValue of filterArray) {
62 | filterOb[findValue] = true
63 | }
64 | let max = null
65 | for (let findOb of findArray) {
66 | if (filterOb[findOb[findKey]]) {
67 | if (!max) {
68 | max = findOb
69 | }
70 | if (findOb[findKey] === value) {
71 | return findOb
72 | }
73 | }
74 | }
75 | return max
76 | }
77 | export default VideoDownloadAdapter
78 |
--------------------------------------------------------------------------------
/src/renderer/video-download-adapter/index.js:
--------------------------------------------------------------------------------
1 | import BiliBiliVideoDownloadAdapter from './BiliBiliVideoDownloadAdapter'
2 | import HuyaVideoDownloadAdapter from './HuyaVideoDownloadAdapter'
3 |
4 | export default {
5 | adapters: [
6 | BiliBiliVideoDownloadAdapter,
7 | HuyaVideoDownloadAdapter
8 | ],
9 | getAdapter (code) {
10 | for (let adapter of this.adapters) {
11 | if (adapter.info.code === code) {
12 | return adapter
13 | }
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/static/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zbfzn/MultiPlatformLiveVideoRecorder/502d4757d026a06cebf5681387fcd7380e77c4b7/static/.gitkeep
--------------------------------------------------------------------------------
/static/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zbfzn/MultiPlatformLiveVideoRecorder/502d4757d026a06cebf5681387fcd7380e77c4b7/static/icon.ico
--------------------------------------------------------------------------------
/static/icon32.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zbfzn/MultiPlatformLiveVideoRecorder/502d4757d026a06cebf5681387fcd7380e77c4b7/static/icon32.ico
--------------------------------------------------------------------------------
/static/styles/demo.css:
--------------------------------------------------------------------------------
1 | /* Logo 字体 */
2 | @font-face {
3 | font-family: "iconfont logo";
4 | src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834');
5 | src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'),
6 | url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'),
7 | url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'),
8 | url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg');
9 | }
10 |
11 | .logo {
12 | font-family: "iconfont logo";
13 | font-size: 160px;
14 | font-style: normal;
15 | -webkit-font-smoothing: antialiased;
16 | -moz-osx-font-smoothing: grayscale;
17 | }
18 |
19 | /* tabs */
20 | .nav-tabs {
21 | position: relative;
22 | }
23 |
24 | .nav-tabs .nav-more {
25 | position: absolute;
26 | right: 0;
27 | bottom: 0;
28 | height: 42px;
29 | line-height: 42px;
30 | color: #666;
31 | }
32 |
33 | #tabs {
34 | border-bottom: 1px solid #eee;
35 | }
36 |
37 | #tabs li {
38 | cursor: pointer;
39 | width: 100px;
40 | height: 40px;
41 | line-height: 40px;
42 | text-align: center;
43 | font-size: 16px;
44 | border-bottom: 2px solid transparent;
45 | position: relative;
46 | z-index: 1;
47 | margin-bottom: -1px;
48 | color: #666;
49 | }
50 |
51 |
52 | #tabs .active {
53 | border-bottom-color: #f00;
54 | color: #222;
55 | }
56 |
57 | .tab-container .content {
58 | display: none;
59 | }
60 |
61 | /* 页面布局 */
62 | .main {
63 | padding: 30px 100px;
64 | width: 960px;
65 | margin: 0 auto;
66 | }
67 |
68 | .main .logo {
69 | color: #333;
70 | text-align: left;
71 | margin-bottom: 30px;
72 | line-height: 1;
73 | height: 110px;
74 | margin-top: -50px;
75 | overflow: hidden;
76 | *zoom: 1;
77 | }
78 |
79 | .main .logo a {
80 | font-size: 160px;
81 | color: #333;
82 | }
83 |
84 | .helps {
85 | margin-top: 40px;
86 | }
87 |
88 | .helps pre {
89 | padding: 20px;
90 | margin: 10px 0;
91 | border: solid 1px #e7e1cd;
92 | background-color: #fffdef;
93 | overflow: auto;
94 | }
95 |
96 | .icon_lists {
97 | width: 100% !important;
98 | overflow: hidden;
99 | *zoom: 1;
100 | }
101 |
102 | .icon_lists li {
103 | width: 100px;
104 | margin-bottom: 10px;
105 | margin-right: 20px;
106 | text-align: center;
107 | list-style: none !important;
108 | cursor: default;
109 | }
110 |
111 | .icon_lists li .code-name {
112 | line-height: 1.2;
113 | }
114 |
115 | .icon_lists .icon {
116 | display: block;
117 | height: 100px;
118 | line-height: 100px;
119 | font-size: 42px;
120 | margin: 10px auto;
121 | color: #333;
122 | -webkit-transition: font-size 0.25s linear, width 0.25s linear;
123 | -moz-transition: font-size 0.25s linear, width 0.25s linear;
124 | transition: font-size 0.25s linear, width 0.25s linear;
125 | }
126 |
127 | .icon_lists .icon:hover {
128 | font-size: 100px;
129 | }
130 |
131 | .icon_lists .svg-icon {
132 | /* 通过设置 font-size 来改变图标大小 */
133 | width: 1em;
134 | /* 图标和文字相邻时,垂直对齐 */
135 | vertical-align: -0.15em;
136 | /* 通过设置 color 来改变 SVG 的颜色/fill */
137 | fill: currentColor;
138 | /* path 和 stroke 溢出 viewBox 部分在 IE 下会显示
139 | normalize.css 中也包含这行 */
140 | overflow: hidden;
141 | }
142 |
143 | .icon_lists li .name,
144 | .icon_lists li .code-name {
145 | color: #666;
146 | }
147 |
148 | /* markdown 样式 */
149 | .markdown {
150 | color: #666;
151 | font-size: 14px;
152 | line-height: 1.8;
153 | }
154 |
155 | .highlight {
156 | line-height: 1.5;
157 | }
158 |
159 | .markdown img {
160 | vertical-align: middle;
161 | max-width: 100%;
162 | }
163 |
164 | .markdown h1 {
165 | color: #404040;
166 | font-weight: 500;
167 | line-height: 40px;
168 | margin-bottom: 24px;
169 | }
170 |
171 | .markdown h2,
172 | .markdown h3,
173 | .markdown h4,
174 | .markdown h5,
175 | .markdown h6 {
176 | color: #404040;
177 | margin: 1.6em 0 0.6em 0;
178 | font-weight: 500;
179 | clear: both;
180 | }
181 |
182 | .markdown h1 {
183 | font-size: 28px;
184 | }
185 |
186 | .markdown h2 {
187 | font-size: 22px;
188 | }
189 |
190 | .markdown h3 {
191 | font-size: 16px;
192 | }
193 |
194 | .markdown h4 {
195 | font-size: 14px;
196 | }
197 |
198 | .markdown h5 {
199 | font-size: 12px;
200 | }
201 |
202 | .markdown h6 {
203 | font-size: 12px;
204 | }
205 |
206 | .markdown hr {
207 | height: 1px;
208 | border: 0;
209 | background: #e9e9e9;
210 | margin: 16px 0;
211 | clear: both;
212 | }
213 |
214 | .markdown p {
215 | margin: 1em 0;
216 | }
217 |
218 | .markdown>p,
219 | .markdown>blockquote,
220 | .markdown>.highlight,
221 | .markdown>ol,
222 | .markdown>ul {
223 | width: 80%;
224 | }
225 |
226 | .markdown ul>li {
227 | list-style: circle;
228 | }
229 |
230 | .markdown>ul li,
231 | .markdown blockquote ul>li {
232 | margin-left: 20px;
233 | padding-left: 4px;
234 | }
235 |
236 | .markdown>ul li p,
237 | .markdown>ol li p {
238 | margin: 0.6em 0;
239 | }
240 |
241 | .markdown ol>li {
242 | list-style: decimal;
243 | }
244 |
245 | .markdown>ol li,
246 | .markdown blockquote ol>li {
247 | margin-left: 20px;
248 | padding-left: 4px;
249 | }
250 |
251 | .markdown code {
252 | margin: 0 3px;
253 | padding: 0 5px;
254 | background: #eee;
255 | border-radius: 3px;
256 | }
257 |
258 | .markdown strong,
259 | .markdown b {
260 | font-weight: 600;
261 | }
262 |
263 | .markdown>table {
264 | border-collapse: collapse;
265 | border-spacing: 0px;
266 | empty-cells: show;
267 | border: 1px solid #e9e9e9;
268 | width: 95%;
269 | margin-bottom: 24px;
270 | }
271 |
272 | .markdown>table th {
273 | white-space: nowrap;
274 | color: #333;
275 | font-weight: 600;
276 | }
277 |
278 | .markdown>table th,
279 | .markdown>table td {
280 | border: 1px solid #e9e9e9;
281 | padding: 8px 16px;
282 | text-align: left;
283 | }
284 |
285 | .markdown>table th {
286 | background: #F7F7F7;
287 | }
288 |
289 | .markdown blockquote {
290 | font-size: 90%;
291 | color: #999;
292 | border-left: 4px solid #e9e9e9;
293 | padding-left: 0.8em;
294 | margin: 1em 0;
295 | }
296 |
297 | .markdown blockquote p {
298 | margin: 0;
299 | }
300 |
301 | .markdown .anchor {
302 | opacity: 0;
303 | transition: opacity 0.3s ease;
304 | margin-left: 8px;
305 | }
306 |
307 | .markdown .waiting {
308 | color: #ccc;
309 | }
310 |
311 | .markdown h1:hover .anchor,
312 | .markdown h2:hover .anchor,
313 | .markdown h3:hover .anchor,
314 | .markdown h4:hover .anchor,
315 | .markdown h5:hover .anchor,
316 | .markdown h6:hover .anchor {
317 | opacity: 1;
318 | display: inline-block;
319 | }
320 |
321 | .markdown>br,
322 | .markdown>p>br {
323 | clear: both;
324 | }
325 |
326 |
327 | .hljs {
328 | display: block;
329 | background: white;
330 | padding: 0.5em;
331 | color: #333333;
332 | overflow-x: auto;
333 | }
334 |
335 | .hljs-comment,
336 | .hljs-meta {
337 | color: #969896;
338 | }
339 |
340 | .hljs-string,
341 | .hljs-variable,
342 | .hljs-template-variable,
343 | .hljs-strong,
344 | .hljs-emphasis,
345 | .hljs-quote {
346 | color: #df5000;
347 | }
348 |
349 | .hljs-keyword,
350 | .hljs-selector-tag,
351 | .hljs-type {
352 | color: #a71d5d;
353 | }
354 |
355 | .hljs-literal,
356 | .hljs-symbol,
357 | .hljs-bullet,
358 | .hljs-attribute {
359 | color: #0086b3;
360 | }
361 |
362 | .hljs-section,
363 | .hljs-name {
364 | color: #63a35c;
365 | }
366 |
367 | .hljs-tag {
368 | color: #333333;
369 | }
370 |
371 | .hljs-title,
372 | .hljs-attr,
373 | .hljs-selector-id,
374 | .hljs-selector-class,
375 | .hljs-selector-attr,
376 | .hljs-selector-pseudo {
377 | color: #795da3;
378 | }
379 |
380 | .hljs-addition {
381 | color: #55a532;
382 | background-color: #eaffea;
383 | }
384 |
385 | .hljs-deletion {
386 | color: #bd2c00;
387 | background-color: #ffecec;
388 | }
389 |
390 | .hljs-link {
391 | text-decoration: underline;
392 | }
393 |
394 | /* 代码高亮 */
395 | /* PrismJS 1.15.0
396 | https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */
397 | /**
398 | * prism.js default theme for JavaScript, CSS and HTML
399 | * Based on dabblet (http://dabblet.com)
400 | * @author Lea Verou
401 | */
402 | code[class*="language-"],
403 | pre[class*="language-"] {
404 | color: black;
405 | background: none;
406 | text-shadow: 0 1px white;
407 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
408 | text-align: left;
409 | white-space: pre;
410 | word-spacing: normal;
411 | word-break: normal;
412 | word-wrap: normal;
413 | line-height: 1.5;
414 |
415 | -moz-tab-size: 4;
416 | -o-tab-size: 4;
417 | tab-size: 4;
418 |
419 | -webkit-hyphens: none;
420 | -moz-hyphens: none;
421 | -ms-hyphens: none;
422 | hyphens: none;
423 | }
424 |
425 | pre[class*="language-"]::-moz-selection,
426 | pre[class*="language-"] ::-moz-selection,
427 | code[class*="language-"]::-moz-selection,
428 | code[class*="language-"] ::-moz-selection {
429 | text-shadow: none;
430 | background: #b3d4fc;
431 | }
432 |
433 | pre[class*="language-"]::selection,
434 | pre[class*="language-"] ::selection,
435 | code[class*="language-"]::selection,
436 | code[class*="language-"] ::selection {
437 | text-shadow: none;
438 | background: #b3d4fc;
439 | }
440 |
441 | @media print {
442 |
443 | code[class*="language-"],
444 | pre[class*="language-"] {
445 | text-shadow: none;
446 | }
447 | }
448 |
449 | /* Code blocks */
450 | pre[class*="language-"] {
451 | padding: 1em;
452 | margin: .5em 0;
453 | overflow: auto;
454 | }
455 |
456 | :not(pre)>code[class*="language-"],
457 | pre[class*="language-"] {
458 | background: #f5f2f0;
459 | }
460 |
461 | /* Inline code */
462 | :not(pre)>code[class*="language-"] {
463 | padding: .1em;
464 | border-radius: .3em;
465 | white-space: normal;
466 | }
467 |
468 | .token.comment,
469 | .token.prolog,
470 | .token.doctype,
471 | .token.cdata {
472 | color: slategray;
473 | }
474 |
475 | .token.punctuation {
476 | color: #999;
477 | }
478 |
479 | .namespace {
480 | opacity: .7;
481 | }
482 |
483 | .token.property,
484 | .token.tag,
485 | .token.boolean,
486 | .token.number,
487 | .token.constant,
488 | .token.symbol,
489 | .token.deleted {
490 | color: #905;
491 | }
492 |
493 | .token.selector,
494 | .token.attr-name,
495 | .token.string,
496 | .token.char,
497 | .token.builtin,
498 | .token.inserted {
499 | color: #690;
500 | }
501 |
502 | .token.operator,
503 | .token.entity,
504 | .token.url,
505 | .language-css .token.string,
506 | .style .token.string {
507 | color: #9a6e3a;
508 | background: hsla(0, 0%, 100%, .5);
509 | }
510 |
511 | .token.atrule,
512 | .token.attr-value,
513 | .token.keyword {
514 | color: #07a;
515 | }
516 |
517 | .token.function,
518 | .token.class-name {
519 | color: #DD4A68;
520 | }
521 |
522 | .token.regex,
523 | .token.important,
524 | .token.variable {
525 | color: #e90;
526 | }
527 |
528 | .token.important,
529 | .token.bold {
530 | font-weight: bold;
531 | }
532 |
533 | .token.italic {
534 | font-style: italic;
535 | }
536 |
537 | .token.entity {
538 | cursor: help;
539 | }
540 |
--------------------------------------------------------------------------------
/static/styles/iconfont.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: "iconfont"; /* Project id 2955134 */
3 | src: url('iconfont.woff2?t=1647222512767') format('woff2'),
4 | url('iconfont.woff?t=1647222512767') format('woff'),
5 | url('iconfont.ttf?t=1647222512767') format('truetype');
6 | }
7 |
8 | [class*="el-icon-zb-"], [class^="el-icon-zb-"] {
9 | font-family: "iconfont" !important;
10 | font-size: 16px;
11 | font-style: normal;
12 | -webkit-font-smoothing: antialiased;
13 | -moz-osx-font-smoothing: grayscale;
14 | }
15 |
16 | .el-icon-zb-zhiding:before {
17 | content: "\eb16";
18 | }
19 |
20 | .el-icon-zb-quxiaozhiding:before {
21 | content: "\e6d8";
22 | }
23 |
24 | .el-icon-zb-start:before {
25 | content: "\e615";
26 | }
27 |
28 | .el-icon-zb-history:before {
29 | content: "\e60a";
30 | }
31 |
32 | .el-icon-zb-help:before {
33 | content: "\e624";
34 | }
35 |
36 | .el-icon-zb-record_create:before {
37 | content: "\e723";
38 | }
39 |
40 | .el-icon-zb-record:before {
41 | content: "\e60b";
42 | }
43 |
44 | .el-icon-zb-videorecorder:before {
45 | content: "\ea44";
46 | }
47 |
48 | .el-icon-zb-recorder:before {
49 | content: "\e7c2";
50 | }
51 |
52 | .el-icon-zb-clear:before {
53 | content: "\e900";
54 | }
55 |
56 | .el-icon-zb-qiyong:before {
57 | content: "\e61e";
58 | }
59 |
60 | .el-icon-zb-jinyong:before {
61 | content: "\e61f";
62 | }
63 |
64 | .el-icon-zb-stop:before {
65 | content: "\e885";
66 | }
67 |
68 |
--------------------------------------------------------------------------------
/static/styles/iconfont.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "2955134",
3 | "name": "live-video-recorder",
4 | "font_family": "iconfont",
5 | "css_prefix_text": "el-icon-zb-",
6 | "description": "",
7 | "glyphs": [
8 | {
9 | "icon_id": "5387616",
10 | "name": "置顶",
11 | "font_class": "zhiding",
12 | "unicode": "eb16",
13 | "unicode_decimal": 60182
14 | },
15 | {
16 | "icon_id": "16682825",
17 | "name": "取消置顶",
18 | "font_class": "quxiaozhiding",
19 | "unicode": "e6d8",
20 | "unicode_decimal": 59096
21 | },
22 | {
23 | "icon_id": "18194347",
24 | "name": "start",
25 | "font_class": "start",
26 | "unicode": "e615",
27 | "unicode_decimal": 58901
28 | },
29 | {
30 | "icon_id": "712657",
31 | "name": "history",
32 | "font_class": "history",
33 | "unicode": "e60a",
34 | "unicode_decimal": 58890
35 | },
36 | {
37 | "icon_id": "11835334",
38 | "name": "help",
39 | "font_class": "help",
40 | "unicode": "e624",
41 | "unicode_decimal": 58916
42 | },
43 | {
44 | "icon_id": "7685225",
45 | "name": "record_create",
46 | "font_class": "record_create",
47 | "unicode": "e723",
48 | "unicode_decimal": 59171
49 | },
50 | {
51 | "icon_id": "8370800",
52 | "name": "record",
53 | "font_class": "record",
54 | "unicode": "e60b",
55 | "unicode_decimal": 58891
56 | },
57 | {
58 | "icon_id": "12002813",
59 | "name": "video recorder",
60 | "font_class": "videorecorder",
61 | "unicode": "ea44",
62 | "unicode_decimal": 59972
63 | },
64 | {
65 | "icon_id": "14342002",
66 | "name": "recorder",
67 | "font_class": "recorder",
68 | "unicode": "e7c2",
69 | "unicode_decimal": 59330
70 | },
71 | {
72 | "icon_id": "8094805",
73 | "name": "clear",
74 | "font_class": "clear",
75 | "unicode": "e900",
76 | "unicode_decimal": 59648
77 | },
78 | {
79 | "icon_id": "5688929",
80 | "name": "启用",
81 | "font_class": "qiyong",
82 | "unicode": "e61e",
83 | "unicode_decimal": 58910
84 | },
85 | {
86 | "icon_id": "5689873",
87 | "name": "禁用",
88 | "font_class": "jinyong",
89 | "unicode": "e61f",
90 | "unicode_decimal": 58911
91 | },
92 | {
93 | "icon_id": "18991752",
94 | "name": "stop",
95 | "font_class": "stop",
96 | "unicode": "e885",
97 | "unicode_decimal": 59525
98 | }
99 | ]
100 | }
101 |
--------------------------------------------------------------------------------
/static/styles/iconfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zbfzn/MultiPlatformLiveVideoRecorder/502d4757d026a06cebf5681387fcd7380e77c4b7/static/styles/iconfont.ttf
--------------------------------------------------------------------------------
/static/styles/iconfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zbfzn/MultiPlatformLiveVideoRecorder/502d4757d026a06cebf5681387fcd7380e77c4b7/static/styles/iconfont.woff
--------------------------------------------------------------------------------
/static/styles/iconfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zbfzn/MultiPlatformLiveVideoRecorder/502d4757d026a06cebf5681387fcd7380e77c4b7/static/styles/iconfont.woff2
--------------------------------------------------------------------------------
/static/tray.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zbfzn/MultiPlatformLiveVideoRecorder/502d4757d026a06cebf5681387fcd7380e77c4b7/static/tray.ico
--------------------------------------------------------------------------------
/static/tray.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zbfzn/MultiPlatformLiveVideoRecorder/502d4757d026a06cebf5681387fcd7380e77c4b7/static/tray.png
--------------------------------------------------------------------------------