├── bilibiliLiveCatch.bat
├── dependent
├── README.md
└── ffmpeg
│ └── README.md
├── output
└── README.md
├── icon.ico
├── bilibiliLiveCatch(x32).exe
├── bilibiliLiveCatch(x64).exe
├── app
├── src
│ ├── public
│ │ ├── icon
│ │ │ ├── fonts
│ │ │ │ ├── icons.ttf
│ │ │ │ ├── icons.woff
│ │ │ │ └── icons.svg
│ │ │ └── style.sass
│ │ ├── common.sass
│ │ ├── config.coffee
│ │ └── function.coffee
│ └── modules
│ │ ├── searchID
│ │ ├── components
│ │ │ ├── data.coffee
│ │ │ ├── style.sass
│ │ │ ├── getHtml.coffee
│ │ │ └── methods.coffee
│ │ └── entry
│ │ │ ├── searchID.coffee
│ │ │ └── searchID.pug
│ │ ├── cut
│ │ ├── entry
│ │ │ ├── cut.coffee
│ │ │ └── cut.pug
│ │ └── components
│ │ │ ├── data.coffee
│ │ │ ├── computingTime.coffee
│ │ │ ├── childListener.coffee
│ │ │ ├── style.sass
│ │ │ └── methods.coffee
│ │ └── index
│ │ ├── components
│ │ ├── data.coffee
│ │ ├── childListener.coffee
│ │ ├── style.sass
│ │ ├── recordVideo.coffee
│ │ └── methods.coffee
│ │ └── entry
│ │ ├── index.coffee
│ │ └── index.pug
├── .gitignore
├── config
│ ├── sass.config.js
│ ├── webpack.dll.js
│ ├── htmlWebpackPlugin.js
│ ├── webpack.dev.js
│ ├── webpack.pro.js
│ └── webpack.config.js
└── package.json
├── .gitignore
├── README.md
└── LICENSE
/bilibiliLiveCatch.bat:
--------------------------------------------------------------------------------
1 | start nw.exe ./app
--------------------------------------------------------------------------------
/dependent/README.md:
--------------------------------------------------------------------------------
1 | # 存放其他依赖文件的目录
2 |
3 | * ffmpeg
--------------------------------------------------------------------------------
/output/README.md:
--------------------------------------------------------------------------------
1 | ## 视频输出文件夹
2 |
3 | 直播视频会输出到此文件夹中。
--------------------------------------------------------------------------------
/dependent/ffmpeg/README.md:
--------------------------------------------------------------------------------
1 | ## ffmpeg文件夹
2 |
3 | 请自行下载对应的ffmpeg版本到此文件夹中。
--------------------------------------------------------------------------------
/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/duan602728596/bilibiliLiveCatch/HEAD/icon.ico
--------------------------------------------------------------------------------
/bilibiliLiveCatch(x32).exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/duan602728596/bilibiliLiveCatch/HEAD/bilibiliLiveCatch(x32).exe
--------------------------------------------------------------------------------
/bilibiliLiveCatch(x64).exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/duan602728596/bilibiliLiveCatch/HEAD/bilibiliLiveCatch(x64).exe
--------------------------------------------------------------------------------
/app/src/public/icon/fonts/icons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/duan602728596/bilibiliLiveCatch/HEAD/app/src/public/icon/fonts/icons.ttf
--------------------------------------------------------------------------------
/app/src/public/icon/fonts/icons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/duan602728596/bilibiliLiveCatch/HEAD/app/src/public/icon/fonts/icons.woff
--------------------------------------------------------------------------------
/app/src/modules/searchID/components/data.coffee:
--------------------------------------------------------------------------------
1 | data = {
2 | 'inputUrl': '', # 地址
3 | 'id': '', # id
4 | 'warnUrl': false,
5 | 'error' : null, # 错误
6 | }
7 |
8 | export default data
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | # node
2 | node_modules
3 | npm-debug.log
4 | package-lock.json
5 |
6 | # yarn
7 | yarn.lock
8 | yarn-error.log
9 |
10 | # webstorm
11 | .idea
12 | app/.idea
13 |
14 | # webpack
15 | .happypack
16 | .dll
17 | build
18 | .babelCache
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # nwjs
2 | *.html
3 | *.dll
4 | *.log
5 | *.nexe
6 | *.bin
7 | *.pak
8 | *.dat
9 | chromedriver.exe
10 | nw.exe
11 | nwjc.exe
12 | payload.exe
13 | pnacl
14 | locales
15 |
16 | # dependent
17 | dependent/ffmpeg/ffmpeg.exe
18 | dependent/ffmpeg/licenses
19 |
20 | # output
21 | output/*.flv
22 | output/*.mp4
23 |
24 | # webstorm
25 | .idea
--------------------------------------------------------------------------------
/app/src/modules/cut/entry/cut.coffee:
--------------------------------------------------------------------------------
1 | import Vue from 'vue/dist/vue'
2 | import '../components/style.sass'
3 | import '../../../public/icon/style.sass'
4 | import data from '../components/data.coffee'
5 | import methods from '../components/methods.coffee'
6 |
7 | # 初始化vue
8 | app = new Vue({
9 | 'el': '#vue-app',
10 | data,
11 | methods,
12 | })
--------------------------------------------------------------------------------
/app/src/modules/searchID/entry/searchID.coffee:
--------------------------------------------------------------------------------
1 | import Vue from 'vue/dist/vue'
2 | import '../components/style.sass'
3 | import '../../../public/icon/style.sass'
4 | import data from '../components/data.coffee'
5 | import methods from '../components/methods.coffee'
6 |
7 |
8 | # 初始化vue
9 | app = new Vue({
10 | 'el': '#vue-app',
11 | data,
12 | methods,
13 | })
--------------------------------------------------------------------------------
/app/src/modules/searchID/components/style.sass:
--------------------------------------------------------------------------------
1 | @import '../../../public/common'
2 |
3 | .search
4 | padding: 30px 20px
5 | &-id
6 | background-color: #fff !important
7 | &-copy
8 | position: relative
9 | z-index: 1
10 | border-radius: 0 .25rem .25rem 0
11 | &-id, &-copy
12 | &:focus
13 | z-index: 3 !important
14 | &-mg
15 | margin-bottom: 40px
--------------------------------------------------------------------------------
/app/src/modules/index/components/data.coffee:
--------------------------------------------------------------------------------
1 | data = {
2 | 'addShow': false, # 弹出层显示隐藏控制
3 | 'idList': [], # 数据列表
4 | 'name': '', # 填写表单的name
5 | 'id': '', # 填写表单的id
6 | 'tableLoading': true, # 表格加载动画
7 | 'warnName': false, # 警告框
8 | 'warnID': false,
9 | 'warnIsReset': false,
10 | 'item': null, # 当前选中的item
11 | }
12 |
13 | export default data
--------------------------------------------------------------------------------
/app/src/modules/cut/components/data.coffee:
--------------------------------------------------------------------------------
1 | data = {
2 | 'cutShow': false, # 弹出层显示隐藏控制
3 | 'video': null, # 视频剪切相关
4 | 'startHour': '',
5 | 'startMinute': '',
6 | 'startSecond': '',
7 | 'endHour': '',
8 | 'endMinute': '',
9 | 'endSecond': '',
10 | 'file': null,
11 | 'saveas': '',
12 | 'cutList': [], # 剪切列表
13 | 'item': null, # 删除
14 | }
15 |
16 | export default data
--------------------------------------------------------------------------------
/app/src/public/common.sass:
--------------------------------------------------------------------------------
1 | .font12
2 | font-size: 12px
3 | .cur-p
4 | cursor: pointer
5 | .mr
6 | margin-right: 5px
7 | /* alert */
8 | .alert-body
9 | $w: 150px
10 | position: absolute
11 | z-index: 3
12 | top: 10px
13 | left: 50%
14 | width: $w
15 | margin-left: $w / -2
16 | /* 动画 */
17 | .fade
18 | &-enter-active, &-leave-active
19 | opacity: 1
20 | transition: opacity .2s
21 | &-enter, &-leave-to
22 | opacity: 0
--------------------------------------------------------------------------------
/app/config/sass.config.js:
--------------------------------------------------------------------------------
1 | /* sass-loader 配置 */
2 | const process = require('process');
3 |
4 | function output(env){
5 | switch(env){
6 | case 'development': // 开发环境
7 | return 'compact';
8 | case 'production': // 生产环境
9 | return 'compressed';
10 | }
11 | }
12 |
13 | // 根据当前环境配置sass输出格式
14 | const env = process.env.NODE_ENV;
15 | const out = output(env);
16 |
17 | module.exports = {
18 | loader: 'sass-loader',
19 | options: {
20 | outputStyle: out
21 | }
22 | };
--------------------------------------------------------------------------------
/app/src/modules/searchID/components/getHtml.coffee:
--------------------------------------------------------------------------------
1 | request = global.require('request')
2 |
3 | # 获取数据
4 | export getData = (id)->
5 | return new Promise((resolve, reject)=>
6 | request({
7 | 'url': 'https://api.live.bilibili.com/room/v1/Room/room_init?id=' + id,
8 | 'method': 'GET',
9 | 'encoding': 'utf-8',
10 | }, (err, res, body)=>
11 | if err
12 | console.error(err)
13 | reject(err)
14 | else
15 | resolve([res, body])
16 | )
17 | )
--------------------------------------------------------------------------------
/app/src/modules/cut/components/computingTime.coffee:
--------------------------------------------------------------------------------
1 | ###
2 | 计算时间差
3 | @param { Array } startTime: 开始时间
4 | @param { Array } endTime : 结束时间
5 | @return { Array }
6 | ###
7 | computingTime = (startTime, endTime)->
8 | startS = startTime[0] * 3600 + startTime[1] * 60 + startTime[2]
9 | endS = endTime[0] * 3600 + endTime[1] * 60 + endTime[2]
10 | cha = endS - startS
11 | h = Number("#{ cha / 3600 }".match(/\d+/g)[0])
12 | hp = cha % 3600
13 | m = Number("#{ hp / 60 }".match(/\d+/g)[0])
14 | s = hp % 60
15 | return [h, m, s]
16 |
17 | export default computingTime
18 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # B站直播流抓取工具
2 | B站直播流抓取功能,以及视频的快速剪切功能。
3 |
4 | ## ROOMID获取方法
5 | ROOMID查看方式:进入B站直播间 -> 右键 -> 查看源代码 -> 第24行
6 | B站直播已改版,ROOMID获取方式如下:
7 | https://api.live.bilibili.com/room/v1/Room/room_init?id={{ ID }},GET请求。
8 |
9 | ## 许可证
10 | 本软件遵循**Apache License 2.0**许可证。
11 |
12 | ## 技术栈
13 | pug + sass + coffeescript + vue + bootstrap + webpack + nwjs。
14 |
15 | ## 目录结构
16 | * nwjs: nwjs SDK
17 | * app: 源代码
18 | * dependent: 依赖的文件存储目录
19 | * ffmpeg: ffmpeg
20 | * output: 视频输出目录
21 |
22 | ## 源代码托管地址
23 | [https://github.com/duan602728596/bilibiliLiveCatch](https://github.com/duan602728596/bilibiliLiveCatch)
--------------------------------------------------------------------------------
/app/src/modules/cut/components/childListener.coffee:
--------------------------------------------------------------------------------
1 | # child进程的监听函数
2 | import { isReset } from '../../../public/function.coffee'
3 |
4 | cb = (item)->
5 | index = isReset(@cutList, 'id', item.id, 0, @cutList.length - 1)
6 | delete item.child
7 | item.isEnd = true
8 | @cutList.splice(index, 1, item)
9 |
10 | export stdout = (item, data)->
11 | # console.log(data.toString())
12 |
13 | export stderr = (item, data)->
14 | # console.log(data.toString())
15 |
16 | export exit = (item, code, data)->
17 | console.log(code + ' ' + data)
18 | cb.call(@, item)
19 |
20 | export error = (item, err)->
21 | console.error(err)
22 | cb.call(@, item)
--------------------------------------------------------------------------------
/app/src/modules/index/components/childListener.coffee:
--------------------------------------------------------------------------------
1 | # child进程的监听函数
2 | import { isReset } from '../../../public/function.coffee'
3 |
4 | cb = (item)->
5 | index = isReset(@idList, 'id', item.id, 0, @idList.length - 1)
6 | delete item.title
7 | delete item.child
8 | @idList.splice(index, 1, item)
9 |
10 | export stdout = (item, data)->
11 | # console.log(data.toString())
12 |
13 | export stderr = (item, data)->
14 | # console.log(data.toString())
15 |
16 | export exit = (item, code, data)->
17 | console.log(code + ' ' + data)
18 | cb.call(@, item)
19 |
20 | export error = (item, err)->
21 | console.error(err)
22 | cb.call(@, item)
--------------------------------------------------------------------------------
/app/config/webpack.dll.js:
--------------------------------------------------------------------------------
1 | /* 预先编译dll */
2 | const path = require('path');
3 | const process = require('process');
4 | const webpack = require('webpack');
5 |
6 | module.exports = {
7 | mode: process.env.NODE_ENV,
8 | entry: {
9 | 'dll': [
10 | 'vue/dist/vue',
11 | 'indexeddb-tools'
12 | ]
13 | },
14 | output: {
15 | path: path.join(__dirname, '../.dll'),
16 | filename: '[name].js',
17 | library: '[name]_[hash]',
18 | libraryTarget: 'var'
19 | },
20 | plugins: [
21 | // dll
22 | new webpack.DllPlugin({
23 | path: '.dll/manifest.json',
24 | name: '[name]_[hash]',
25 | context: __dirname
26 | })
27 | ]
28 | };
--------------------------------------------------------------------------------
/app/src/public/config.coffee:
--------------------------------------------------------------------------------
1 | # 配置文件
2 | path = global.require('path')
3 | process = global.require('process');
4 |
5 | execPath = path.dirname(process.execPath).replace(/\\/g, '/')
6 |
7 | config = {
8 | 'indexeddb': {
9 | 'name': 'bilibiliLiveCatch',
10 | 'version': 1,
11 | 'objectStore': {
12 | 'list': {
13 | 'name': 'list',
14 | 'key': 'id',
15 | 'data': [
16 | {
17 | 'name': 'name',
18 | 'index': 'name',
19 | },
20 | ],
21 | },
22 | },
23 | },
24 | 'ffmpeg': execPath + '/dependent/ffmpeg/ffmpeg.exe',
25 | 'output': execPath + '/output',
26 | }
27 |
28 | export default config
--------------------------------------------------------------------------------
/app/src/modules/index/components/style.sass:
--------------------------------------------------------------------------------
1 | @import '../../../public/common'
2 |
3 | /* 顶部工具栏 */
4 | .tools
5 | position: fixed
6 | z-index: 1
7 | top: 0
8 | left: 0
9 | width: 100%
10 | .tools, .data
11 | padding: 10px
12 | /* 数据 */
13 | .data
14 | $w1: 36
15 | $w2: (100 - $w1) / 2
16 | padding-top: 60px
17 | &-table
18 | td, th
19 | vertical-align: middle
20 | &-color-fff
21 | color: #fff
22 | &-w1
23 | width: $w1 * 1%
24 | &-w2
25 | width: $w2 * 1%
26 | &-disabled
27 | cursor: pointer
28 | &[disabled]
29 | cursor: no-drop
30 | /* 弹出层 */
31 | .dialog
32 | z-index: 1
33 | display: block
34 | background-color: rgba(0, 0, 0, .5)
35 | &-title
36 | font-size: 14px
--------------------------------------------------------------------------------
/app/src/modules/index/components/recordVideo.coffee:
--------------------------------------------------------------------------------
1 | request = global.require('request')
2 |
3 | # 获取地址方法
4 | export getUrl = (roomID)->
5 | return new Promise((resolve, reject)=>
6 | request({
7 | 'url': "http://api.live.bilibili.com/api/playurl?cid=#{ roomID }&otype=json&quality=0&platform=web",
8 | 'method': 'GET',
9 | 'headers': {
10 | 'Host': 'live.bilibili.com',
11 | 'X-Requested-With': 'ShockwaveFlash/25.0.0.148',
12 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.81 Safari/537.36',
13 | },
14 | 'encoding': 'utf-8',
15 | }, (err, res, body)=>
16 | if err
17 | reject(err)
18 | else
19 | resolve(body)
20 | )
21 | )
--------------------------------------------------------------------------------
/app/src/modules/cut/components/style.sass:
--------------------------------------------------------------------------------
1 | @import '../../../public/common'
2 |
3 | /* 顶部工具栏 */
4 | .tools
5 | position: fixed
6 | z-index: 1
7 | top: 0
8 | left: 0
9 | width: 100%
10 | .tools, .cutList
11 | padding: 10px
12 | /* 剪切列表 */
13 | .cutList
14 | padding-top: 60px
15 | &-table
16 | td, th
17 | vertical-align: middle
18 | &-color-fff
19 | color: #fff
20 | &-w1
21 | width: 45%
22 | &-w2
23 | width: 25%
24 | &-w3
25 | width: 30%
26 | &-disabled
27 | cursor: pointer
28 | &[disabled]
29 | cursor: no-drop
30 | &-ft
31 | display: inline-block
32 | width: 40px
33 | /* 弹出层 */
34 | .dialog
35 | display: block
36 | background-color: rgba(0, 0, 0, .5)
37 | &-title
38 | font-size: 14px
39 | &-input
40 | display: inline-block
41 | width: 100px
42 | &-dian
43 | margin: 0 5px
44 | &-label
45 | margin-right: 10px
--------------------------------------------------------------------------------
/app/src/modules/searchID/components/methods.coffee:
--------------------------------------------------------------------------------
1 | import { getData } from './getHtml.coffee'
2 | url = global.require('url')
3 |
4 | # 复制
5 | onCopy = ()->
6 | range = document.createRange();
7 | range.selectNode(document.getElementById('id'))
8 | selection = window.getSelection()
9 | if selection.rangeCount > 0
10 | selection.removeAllRanges()
11 | selection.addRange(range)
12 | document.execCommand('copy')
13 |
14 | # 获取ROOMID
15 | onGetRoomId = ()->
16 | u = url.parse(@inputUrl)
17 | if u.host != 'live.bilibili.com'
18 | @warnUrl = true
19 | setTimeout(()=>
20 | @warnUrl = false
21 | , 2000)
22 | return false
23 | id = @inputUrl.split(/\//g)
24 | id2 = id[id.length - 1]
25 | [res, body] = await getData(id2)
26 | body2 = JSON.parse(body)
27 | if res.statusCode == 200 and body2.code == 0
28 | @id = body2.data.room_id
29 | else
30 | @error = body2.msg
31 | setTimeout(()=>
32 | @error = null
33 | , 2000)
34 | return false
35 |
36 |
37 | methods = {
38 | onCopy,
39 | onGetRoomId,
40 | }
41 |
42 | export default methods
--------------------------------------------------------------------------------
/app/config/htmlWebpackPlugin.js:
--------------------------------------------------------------------------------
1 | /* html模板 */
2 | const path = require('path');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const excludeChunks = ['index', 'searchID', 'cut'];
5 |
6 | // 跳过模块
7 | function excludeChunksCopy(name){
8 | const excludeChunks2 = [].concat(excludeChunks);
9 | const index = excludeChunks2.indexOf(name);
10 | excludeChunks2.splice(index, 1);
11 | return excludeChunks2;
12 | }
13 |
14 | // 开发环境
15 | function devHtmlWebpackPlugin(name, file){
16 | return new HtmlWebpackPlugin({
17 | filename: name + '.html',
18 | inject: true,
19 | hash: true,
20 | template: path.join(__dirname, file),
21 | excludeChunks: excludeChunksCopy(name)
22 | })
23 | }
24 |
25 | // 生产环境
26 | function proHtmlWebpackPlugin(name, file){
27 | return new HtmlWebpackPlugin({
28 | filename: name + '.html',
29 | inject: true,
30 | hash: true,
31 | template: path.join(__dirname, file),
32 | minify: {
33 | minifyCSS: true,
34 | minifyJS: true
35 | },
36 | excludeChunks: excludeChunksCopy(name)
37 | })
38 | }
39 |
40 | module.exports = {
41 | devHtmlWebpackPlugin,
42 | proHtmlWebpackPlugin
43 | };
--------------------------------------------------------------------------------
/app/src/modules/index/entry/index.coffee:
--------------------------------------------------------------------------------
1 | import Vue from 'vue/dist/vue'
2 | import IndexedDB from 'indexeddb-tools';
3 | import '../components/style.sass'
4 | import '../../../public/icon/style.sass'
5 | import config from '../../../public/config.coffee'
6 | import data from '../components/data.coffee'
7 | import methods from '../components/methods.coffee'
8 |
9 | # 初始化vue
10 | app = new Vue({
11 | 'el': '#vue-app',
12 | data,
13 | methods,
14 | })
15 |
16 | # 初始化数据库
17 | { name, version, objectStore } = config.indexeddb
18 |
19 | initDbUp = (et, event)->
20 | list = objectStore.list
21 | if not @.hasObjectStore(list.name)
22 | @.createObjectStore(list.name, list.key, list.data)
23 |
24 | initDbSuccess = (et, event)->
25 | _this = @
26 | store = @getObjectStore(objectStore.list.name)
27 | data2 = []
28 | store.cursor('name', (event)->
29 | { result } = event.target
30 | if result
31 | data2.push(result.value)
32 | result.continue()
33 | else
34 | app.idList = data2
35 | app.tableLoading = false
36 | _this.close()
37 | )
38 |
39 | IndexedDB(name, version, {
40 | 'success': initDbSuccess,
41 | 'upgradeneeded': initDbUp,
42 | })
--------------------------------------------------------------------------------
/app/src/modules/searchID/entry/searchID.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | meta(charset='utf-8')
5 | meta(name='viewport' content='width=device-width, initial-scale=1')
6 | link(rel='stylesheet' href=require('bootstrap/dist/css/bootstrap.min.css'))
7 | body
8 | div#vue-app
9 | form.search.font12
10 | div.form-group
11 | label(for='url') 直播间地址:
12 | input.form-control.font12#url(type='text' v-model='inputUrl')
13 | div.form-group.search-mg
14 | label(for='id') 直播间ID:
15 | div.input-group
16 | input.form-control.search-id.font12#id(type='text' v-model='id' readonly)
17 | button.btn.input-group-addon.search-copy.cur-p(type='button' title='复制' v-on:click='onCopy()')
18 | i.icon.icon-clipboard
19 | button.btn.btn-block.btn-lg.btn-primary.font12.cur-p(type='button' title='获取直播间ID' v-on:click='onGetRoomId()')
20 | i.icon.icon-search.mr
21 | | 获取直播间ID
22 | // 弹出层
23 | transition(name='fade')
24 | div.alert.alert-warning.text-center.alert-body.font12(role='alert' v-if='warnUrl') 直播间地址错误!
25 | transition(name='fade')
26 | div.alert.alert-danger.text-center.alert-body.font12(role='alert' v-if='error != null') {{ error }}
27 | script(type='text/javascript' src=require('../../../../.dll/dll'))
--------------------------------------------------------------------------------
/app/src/public/icon/style.sass:
--------------------------------------------------------------------------------
1 | $icomoon-font-path: './fonts' !default
2 |
3 | $icon-film: '\e900'
4 | $icon-film2: '\e902'
5 | $icon-scissors: '\e903'
6 | $icon-clipboard: '\e901'
7 | $icon-search: '\f02e'
8 | $icon-server: '\f097'
9 | $icon-drive: '\e963'
10 |
11 | @font-face
12 | font-family: 'icons'
13 | src: url('#{ $icomoon-font-path }/icons.ttf?qq6qsk') format('truetype'), url('#{ $icomoon-font-path }/icons.woff?qq6qsk') format('woff'), url('#{ $icomoon-font-path }/icons.svg?qq6qsk#icons') format('svg')
14 | font-weight: normal
15 | font-style: normal
16 |
17 | .icon
18 | margin-right: 5px
19 | vertical-align: -1px
20 |
21 | /* use !important to prevent issues with browser extensions that change fonts */
22 | font-family: 'icons' !important
23 | speak: none
24 | font-style: normal
25 | font-weight: normal
26 | font-variant: normal
27 | text-transform: none
28 | line-height: 1
29 |
30 | /* Better Font Rendering =========== */
31 | -webkit-font-smoothing: antialiased
32 | -moz-osx-font-smoothing: grayscale
33 |
34 | .icon-film
35 | &:before
36 | content: $icon-film
37 | .icon-film2
38 | &:before
39 | content: $icon-film2
40 | .icon-scissors
41 | &:before
42 | content: $icon-scissors
43 | .icon-clipboard
44 | &:before
45 | content: $icon-clipboard
46 | .icon-search
47 | &:before
48 | content: $icon-search
49 | .icon-server
50 | &:before
51 | content: $icon-server
52 | .icon-drive
53 | &:before
54 | content: $icon-drive
--------------------------------------------------------------------------------
/app/src/public/function.coffee:
--------------------------------------------------------------------------------
1 | # 公共方法
2 |
3 | # 查重
4 | export isReset = (rawArray, key, value, from, to)->
5 | if rawArray.length == 0
6 | return null
7 |
8 | if from == to
9 | if rawArray[from][key] == value
10 | return from
11 | else
12 | return null
13 |
14 | middle = Math.floor((to - from) / 2) + from
15 |
16 | left = isReset(rawArray, key, value, from, middle)
17 | if left != null
18 | return left
19 |
20 | right = isReset(rawArray, key, value, middle + 1, to)
21 | if right != null
22 | return right
23 |
24 | return null
25 |
26 | # 补零
27 | export zero = (arg)->
28 | num = if typeof arg == 'number' then arg else Number(arg)
29 | return if num > 10 then "#{ num }" else "0#{ num }"
30 |
31 | # 时间
32 | export time = (template, timeStr)->
33 | date = if timeStr then new Date(timeStr) else new Date()
34 | year = date.getFullYear()
35 | month = date.getMonth() + 1
36 | day = date.getDate()
37 | hour = date.getHours()
38 | minute = date.getMinutes()
39 | second = date.getSeconds()
40 | return template.replace(/Y{2}/, year)
41 | .replace(/M{2}/, zero(month))
42 | .replace(/D{2}/, zero(day))
43 | .replace(/h{2}/, zero(hour))
44 | .replace(/m{2}/, zero(minute))
45 | .replace(/s{2}/, zero(second))
46 |
47 | # 获取随机数
48 | STR = '1234567890QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm'
49 | STR_LEN = STR.length
50 | export randomStr = (len = 10)->
51 | str = ''
52 | for index in [0..len]
53 | str += STR[Math.floor(Math.random() * STR_LEN)]
54 | return str
--------------------------------------------------------------------------------
/app/config/webpack.dev.js:
--------------------------------------------------------------------------------
1 | /* 开发环境 */
2 | const path = require('path');
3 | const { devHtmlWebpackPlugin } = require('./htmlWebpackPlugin');
4 | const config = require('./webpack.config');
5 | const sassConfig = require('./sass.config');
6 |
7 | /* 合并配置 */
8 | module.exports = config({
9 | output: {
10 | path: path.join(__dirname, '../build'),
11 | filename: 'script/[name].js',
12 | chunkFilename: 'script/[name]_chunk.js'
13 | },
14 | devtool: 'cheap-module-source-map',
15 | module: {
16 | rules: [
17 | { // sass
18 | test: /^.*\.sass$/,
19 | use: ['style-loader', 'css-loader', sassConfig]
20 | },
21 | { // css
22 | test: /^.*\.css$/,
23 | use: ['style-loader', 'css-loader'],
24 | exclude: /(bootstrap)/
25 | },
26 | { // pug
27 | test: /^.*\.pug$/,
28 | use: [
29 | {
30 | loader: 'pug-loader',
31 | options: {
32 | pretty: true,
33 | name: '[name].html'
34 | }
35 | }
36 | ]
37 | },
38 | { // 矢量图片 & 文字
39 | test: /^.*\.(eot|svg|ttf|woff|woff2)$/,
40 | use: [
41 | {
42 | loader: 'file-loader',
43 | options: {
44 | name: '[name].[ext]',
45 | outputPath: 'file/'
46 | }
47 | }
48 | ]
49 | }
50 | ]
51 | },
52 | plugins: [
53 | // html模板
54 | devHtmlWebpackPlugin('index', '../src/modules/index/entry/index.pug'),
55 | devHtmlWebpackPlugin('searchID', '../src/modules/searchID/entry/searchID.pug'),
56 | devHtmlWebpackPlugin('cut', '../src/modules/cut/entry/cut.pug')
57 | ]
58 | });
--------------------------------------------------------------------------------
/app/config/webpack.pro.js:
--------------------------------------------------------------------------------
1 | /* 生产环境 */
2 | const path = require('path');
3 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
4 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin');
5 | const { proHtmlWebpackPlugin } = require('./htmlWebpackPlugin');
6 | const config = require('./webpack.config');
7 | const sassConfig = require('./sass.config');
8 |
9 | /* 合并配置 */
10 | module.exports = config({
11 | output: {
12 | path: path.join(__dirname, '../build'),
13 | filename: 'script/[name]_[chunkhash].js',
14 | chunkFilename: 'script/[name]_[chunkhash]_chunk.js'
15 | },
16 | module: {
17 | rules: [
18 | { // sass
19 | test: /^.*\.sass$/,
20 | use: ExtractTextPlugin.extract({
21 | fallback: 'style-loader',
22 | use: ['css-loader', sassConfig]
23 | })
24 | },
25 | { // css
26 | test: /^.*\.css$/,
27 | use: ExtractTextPlugin.extract({
28 | fallback: 'style-loader',
29 | use: ['css-loader']
30 | }),
31 | exclude: /(bootstrap)/
32 | },
33 | { // pug
34 | test: /^.*\.pug$/,
35 | use: [
36 | {
37 | loader: 'pug-loader',
38 | options: {
39 | name: '[name].html'
40 | }
41 | }
42 | ]
43 | },
44 | { // 矢量图片 & 文字
45 | test: /^.*\.(eot|svg|ttf|woff|woff2)$/,
46 | use: [
47 | {
48 | loader: 'file-loader',
49 | options: {
50 | name: '[name]_[hash].[ext]',
51 | outputPath: 'file/',
52 | publicPath: '../file/'
53 | }
54 | }
55 | ]
56 | }
57 | ]
58 | },
59 | plugins: [
60 | new ExtractTextPlugin({
61 | filename: 'style/[name]_[contenthash].css',
62 | allChunks: true
63 | }),
64 | new OptimizeCSSPlugin(),
65 | // html模板
66 | proHtmlWebpackPlugin('index', '../src/modules/index/entry/index.pug'),
67 | proHtmlWebpackPlugin('searchID', '../src/modules/searchID/entry/searchID.pug'),
68 | proHtmlWebpackPlugin('cut', '../src/modules/cut/entry/cut.pug')
69 | ]
70 | });
--------------------------------------------------------------------------------
/app/config/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const manifest = require('../.dll/manifest.json');
4 |
5 | function config(options){
6 | const conf = {
7 | mode: process.env.NODE_ENV,
8 | entry: {
9 | index: path.join(__dirname, '../src/modules/index/entry/index.coffee'),
10 | searchID: path.join(__dirname, '../src/modules/searchID/entry/searchID.coffee'),
11 | cut: path.join(__dirname, '../src/modules/cut/entry/cut.coffee')
12 | },
13 | module: {
14 | rules: [
15 | { // coffeescript
16 | test: /^.*\.coffee$/,
17 | use: ['coffee-loader']
18 | },
19 | {
20 | test: /(dll\.js|common\.js)/,
21 | use: [
22 | {
23 | loader: 'file-loader',
24 | options: {
25 | name: '[name]_[hash].[ext]',
26 | outputPath: 'script/'
27 | }
28 | }
29 | ]
30 | },
31 | {
32 | test: /(bootstrap.*\.css)/,
33 | use: [
34 | {
35 | loader: 'file-loader',
36 | options: {
37 | name: '[name]_[hash].[ext]',
38 | outputPath: 'style/'
39 | }
40 | }
41 | ]
42 | },
43 | { // 图片
44 | test: /^.*\.(jpg|png|gif)$/,
45 | use: [
46 | {
47 | loader: 'url-loader',
48 | options: {
49 | limit: 3000,
50 | name: '[name]_[hash].[ext]',
51 | outputPath: 'image/'
52 | }
53 | }
54 | ]
55 | }
56 | ]
57 | },
58 | plugins: [
59 | // dll
60 | new webpack.DllReferencePlugin({
61 | context: __dirname,
62 | manifest: manifest
63 | })
64 | ]
65 | };
66 |
67 | /* 合并 */
68 | conf.module.rules = conf.module.rules.concat(options.module.rules); // 合并rules
69 | conf.plugins = conf.plugins.concat(options.plugins); // 合并插件
70 | conf.output = options.output; // 合并输出目录
71 | if('devtool' in options) conf.devtool = options.devtool; // 合并source-map配置
72 |
73 | return conf;
74 | }
75 |
76 | module.exports = config;
--------------------------------------------------------------------------------
/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bilibiliLiveCatch",
3 | "version": "1.3.0",
4 | "description": "B站直播流抓取工具,带视频的快速剪切功能。",
5 | "license": "Apache License 2.0",
6 | "scripts": {
7 | "start": "npm run cleanbuild && cross-env NODE_ENV=development ./node_modules/.bin/webpack --config ./config/webpack.dev.js --watch",
8 | "build": "npm run cleanbuild && cross-env NODE_ENV=production ./node_modules/.bin/webpack --config ./config/webpack.pro.js --progress",
9 | "cleanbuild": "./node_modules/.bin/rimraf build",
10 | "devdll": "cross-env NODE_ENV=development ./node_modules/.bin/webpack --config ./config/webpack.dll.js --progress",
11 | "prodll": "cross-env NODE_ENV=production ./node_modules/.bin/webpack --config ./config/webpack.dll.js --progress",
12 | "npmi": "npm install --production",
13 | "yarni": "yarn install --production=true --pure-lockfile",
14 | "clean": "node-modules-clean --ext \".opts|.map|.ts|.yml|.png|.dot|.jpg\" --file \"test.js\""
15 | },
16 | "main": "./build/index.html",
17 | "nodejs": true,
18 | "window": {
19 | "title": "B站直播流抓取工具(v1.3.0)",
20 | "position": "center",
21 | "toolbar": true,
22 | "frame": true,
23 | "width": 1200,
24 | "height": 600,
25 | "fullscreen": false,
26 | "show_in_taskbar": true
27 | },
28 | "author": {
29 | "name": "段昊辰",
30 | "email": "duanhaochen@126.com",
31 | "url": "https://github.com/duan602728596"
32 | },
33 | "dependencies": {
34 | "cheerio": "^1.0.0-rc.2",
35 | "request": "^2.83.0"
36 | },
37 | "devDependencies": {
38 | "bootstrap": "^4.0.0",
39 | "coffee-loader": "^0.9.0",
40 | "coffeescript": "^2.2.2",
41 | "cross-env": "^5.1.3",
42 | "css-loader": "^0.28.10",
43 | "extract-text-webpack-plugin": "^4.0.0-beta.0",
44 | "file-loader": "^1.1.11",
45 | "html-webpack-plugin": "^3.0.4",
46 | "indexeddb-tools": "^2.0.3",
47 | "node-sass": "^4.7.2",
48 | "optimize-css-assets-webpack-plugin": "^3.2.0",
49 | "pug": "^2.0.0-rc.4",
50 | "pug-loader": "github:pugjs/pug-loader",
51 | "sass-loader": "^6.0.7",
52 | "style-loader": "^0.20.2",
53 | "rimraf": "^2.6.2",
54 | "uglifyjs-webpack-plugin": "^1.2.2",
55 | "url-loader": "^1.0.1",
56 | "vue": "^2.5.13",
57 | "webpack": "^4.0.1",
58 | "webpack-cli": "^2.0.10"
59 | },
60 | "peerDependencies": {
61 | "node-modules-clean": "^0.1.1"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/app/src/modules/cut/components/methods.coffee:
--------------------------------------------------------------------------------
1 | import { isReset, time, zero, randomStr } from '../../../public/function.coffee'
2 | import { stdout, stderr, exit, error } from './childListener.coffee'
3 | import config from '../../../public/config.coffee'
4 | import computingTime from './computingTime.coffee'
5 | child_process = global.require('child_process')
6 | path = global.require('path')
7 |
8 | # 弹出层显示隐藏控制
9 | dialogDisplay = (value)->
10 | @cutShow = value
11 | if value == false
12 | document.getElementById('dialog').reset()
13 |
14 | # 选择文件
15 | onVideoChange = (event)->
16 | file = event.target.files[0]
17 | @video = if file then file else null
18 | if file
19 | p = path.parse(file.path)
20 | @saveas = '[cut]' + p.name + '-' + time('YYMMDDhhmmss') + p.ext
21 |
22 | onFileChange = (event)->
23 | file = event.target.files[0]
24 | @file = if file then file else null
25 |
26 | # 添加
27 | onAddCut = ()->
28 | x = {
29 | 'id': randomStr(),
30 | 'video': if @video then @video.path else '',
31 | 'file': if @file then @file.path else '',
32 | 'startHour': zero(@startHour),
33 | 'startMinute': zero(@startMinute),
34 | 'startSecond': zero(@startSecond),
35 | 'endHour': zero(@endHour),
36 | 'endMinute': zero(@endMinute),
37 | 'endSecond': zero(@endSecond),
38 | 'isEnd': false,
39 | }
40 | @cutList.push(x)
41 | # 初始化弹出层
42 | @startHour = ''
43 | @startMinute = ''
44 | @startSecond = ''
45 | @endHour = ''
46 | @endMinute = ''
47 | @endSecond = ''
48 | document.getElementById('dialog').reset()
49 |
50 | # 删除
51 | onDelete = (item)->
52 | @item = item
53 |
54 | # 确认删除
55 | onOkDelete = ()->
56 | index = isReset(@cutList, 'id', @item.id, 0, @cutList.length - 1)
57 | @cutList.splice(index, 1)
58 | @item = null
59 |
60 | # 取消删除
61 | onCancelDelete = ()->
62 | @item = null
63 |
64 | # 剪切
65 | onStartCut = (item)->
66 | { ext } = path.parse(item.file)
67 | index = isReset(@cutList, 'id', item.id, 0, @cutList.length - 1)
68 | [h, m, s] = computingTime(
69 | [
70 | Number(item.startHour),
71 | Number(item.startMinute),
72 | Number(item.startSecond),
73 | ],
74 | [
75 | Number(item.endHour),
76 | Number(item.endMinute),
77 | Number(item.endSecond),
78 | ],
79 | )
80 | console.log(ext)
81 | arg = if ext == '.gif' then [
82 | '-ss',
83 | "#{ item.startHour }:#{ item.startMinute }:#{ item.startSecond }",
84 | '-t',
85 | "#{ zero(h) }:#{ zero(m) }:#{ zero(s) }",
86 | '-i',
87 | item.video,
88 | item.file,
89 | ] else [
90 | '-ss',
91 | "#{ item.startHour }:#{ item.startMinute }:#{ item.startSecond }",
92 | '-t',
93 | "#{ zero(h) }:#{ zero(m) }:#{ zero(s) }",
94 | '-accurate_seek',
95 | '-i',
96 | item.video,
97 | '-acodec',
98 | 'copy',
99 | '-vcodec',
100 | 'copy',
101 | item.file,
102 | ]
103 | console.log(arg)
104 | child = child_process.spawn(config.ffmpeg, arg)
105 | Object.assign(item, {
106 | child,
107 | })
108 | child.stdout.on('data', stdout.bind(@, item))
109 | child.stderr.on('data', stderr.bind(@, item))
110 | child.on('exit', exit.bind(@, item))
111 | child.on('error', error.bind(@, item))
112 | @cutList.splice(index, 1, item)
113 |
114 |
115 | methods = {
116 | dialogDisplay,
117 | onAddCut,
118 | onVideoChange,
119 | onFileChange,
120 | onDelete,
121 | onOkDelete,
122 | onCancelDelete,
123 | onStartCut,
124 | }
125 |
126 | export default methods
--------------------------------------------------------------------------------
/app/src/modules/index/components/methods.coffee:
--------------------------------------------------------------------------------
1 | import IndexedDB from 'indexeddb-tools';
2 | import config from '../../../public/config.coffee'
3 | import { isReset, time } from '../../../public/function.coffee'
4 | import { getUrl } from './recordVideo.coffee'
5 | import { stdout, stderr, exit, error } from './childListener.coffee'
6 | gui = global.require('nw.gui')
7 | child_process = global.require('child_process');
8 |
9 | SPACE_REG = /^\s*$/
10 | { name, version, objectStore } = config.indexeddb
11 |
12 | # 弹出层显示隐藏控制
13 | dialogDisplay = (value)->
14 | @addShow = value
15 | if value == false
16 | @name = ''
17 | @id = ''
18 |
19 | # 添加一条数据
20 | addAData = ()->
21 | _this = @
22 | # 表单验证
23 | if SPACE_REG.test(@name)
24 | @warnName = true
25 | setTimeout(()=>
26 | @warnName = false
27 | , 2000)
28 | return false
29 | if SPACE_REG.test(@id)
30 | @warnID = true
31 | setTimeout(()=>
32 | @warnID = false
33 | , 2000)
34 | return false
35 | # 查重
36 | index = isReset(@idList, 'id', @warnID, 0, @idList.length - 1)
37 | if index != null
38 | @warnIsReset = true
39 | setTimeout(()=>
40 | @warnIsReset = false
41 | , 2000)
42 | return false
43 | # 将数据添加到数据库中
44 | IndexedDB(name, version, {
45 | 'success': (et, event)->
46 | store = @getObjectStore(objectStore.list.name, true)
47 | d = {
48 | 'name': _this.name,
49 | 'id': Number(_this.id),
50 | }
51 | store.add(d)
52 | _this.idList.push(d)
53 | _this.name = ''
54 | _this.id = ''
55 | @close()
56 | })
57 |
58 | # 删除
59 | onDelete = (item)->
60 | @item = item
61 |
62 | # 确认删除
63 | onOkDelete = ()->
64 | _this = @
65 | { id } = @item
66 | index = isReset(@idList, 'id', id, 0, @idList.length - 1)
67 | IndexedDB(name, version, {
68 | 'success': (et, event)->
69 | store = @getObjectStore(objectStore.list.name, true)
70 | store.delete(id)
71 | _this.idList.splice(index, 1)
72 | _this.item = null
73 | @close()
74 | })
75 |
76 | # 取消删除
77 | onCancelDelete = ()->
78 | @item = null
79 |
80 | # 打开新窗口
81 | onOpenSearchIDWindow = ()->
82 | gui.Window.open('./build/searchID.html', {
83 | 'position': 'center',
84 | 'width': 500,
85 | 'height': 300,
86 | 'focus': true,
87 | 'title': 'RoomID搜索',
88 | })
89 |
90 | onOpenCutWindow = ()->
91 | gui.Window.open('./build/cut.html', {
92 | 'position': 'center',
93 | 'width': 1200,
94 | 'height': 600,
95 | 'focus': true,
96 | 'title': '视频剪切',
97 | })
98 |
99 | # 录制
100 | onRecording = (item)->
101 | { id } = item
102 | index = isReset(@idList, 'id', id, 0, @idList.length - 1)
103 | result = await getUrl(id)
104 | urlList = JSON.parse(result)
105 | title = "#{ id }_#{ time('YYMMDDhhmmss') }"
106 | child = child_process.spawn(config.ffmpeg, ['-i', urlList.durl[0].url, '-c', 'copy', config.output + '/' + title + '.flv'])
107 | Object.assign(item, {
108 | title,
109 | child,
110 | })
111 | child.stdout.on('data', stdout.bind(@, item))
112 | child.stderr.on('data', stderr.bind(@, item))
113 | child.on('exit', exit.bind(@, item))
114 | child.on('error', error.bind(@, item))
115 | @idList.splice(index, 1, item)
116 |
117 | # 停止录制
118 | onStopRecording = (item)->
119 | index = isReset(@idList, 'id', item.id, 0, @idList.length - 1)
120 | item.child.kill()
121 | delete item.child
122 | delete item.title
123 | @idList.splice(index, 1, item)
124 |
125 |
126 | methods = {
127 | dialogDisplay,
128 | addAData,
129 | onDelete,
130 | onOkDelete,
131 | onCancelDelete,
132 | onOpenSearchIDWindow,
133 | onOpenCutWindow,
134 | onRecording,
135 | onStopRecording,
136 | }
137 |
138 | export default methods
--------------------------------------------------------------------------------
/app/src/modules/index/entry/index.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | meta(charset='utf-8')
5 | meta(name='viewport' content='width=device-width, initial-scale=1')
6 | link(rel='stylesheet' href=require('bootstrap/dist/css/bootstrap.min.css'))
7 | body
8 | div#vue-app
9 | // 顶部工具栏
10 | header.bg-dark.tools
11 | div
12 | button.btn.btn-primary.mr.font12.cur-p(type='button' title='添加直播间ID' v-on:click='dialogDisplay(true)')
13 | i.icon.icon-drive
14 | | 添加直播间ID
15 | button.btn.btn-default.mr.font12.cur-p(type='button' title='查找直播间ID' v-on:click='onOpenSearchIDWindow()')
16 | i.icon.icon-server
17 | | 查找直播间ID
18 | button.btn.btn-default.font12.cur-p(type='button' title='视频快速剪切' v-on:click='onOpenCutWindow()')
19 | i.icon.icon-film
20 | | 视频快速剪切
21 | // 数据
22 | div.data
23 | table.table.table-bordered.table-hover.data-table.font12
24 | thead.bg-primary.data-color-fff
25 | tr
26 | th.data-w1 名称
27 | th.data-w2 ID
28 | th.data-w2 操作
29 | tbody(v-if='idList.length === 0')
30 | tr
31 | td.text-center(colspan='3') 暂无数据
32 | tbody(v-else v-for='(item, index) in idList')
33 | tr
34 | td {{ item.name }}
35 | td {{ item.id }}
36 | td
37 | button.btn.btn-sm.btn-outline-danger.mr.font12.cur-p(type='button' title='停止录制' v-if="'child' in item" v-on:click='onStopRecording(item)') 停止录制
38 | button.btn.btn-sm.btn-outline-dark.mr.font12.cur-p(type='button' title='开始录制' v-else v-on:click='onRecording(item)') 开始录制
39 | button.btn.btn-sm.btn-outline-danger.data-disabled.font12(type='button' title='删除' v-on:click='onDelete(item)' v-bind:disabled="'child' in item") 删除
40 | // 添加一个数据
41 | transition(name='fade')
42 | div.modal.dialog(tabindex='-1' role='dialog' v-if='addShow')
43 | form.modal-dialog.font12(role='document')
44 | div.modal-content
45 | div.modal-header
46 | h5.modal-title.dialog-title 添加直播间ID
47 | div.modal-body
48 | div.form-group
49 | label(for='forms-name') 名称:
50 | input.form-control.font12#forms-name(type='text' v-model='name')
51 | div.form-group
52 | label(for='forms-id') ID:
53 | input.form-control.font12#forms-id(type='text' v-model='id')
54 | div.modal-footer
55 | button.btn.btn-secondary.font12.cur-p(type='button' title='关闭' v-on:click='dialogDisplay(false)') 关闭
56 | button.btn.btn-primary.font12.cur-p(type='button' title='添加' v-on:click='addAData()') 添加
57 | // 确认删除的弹出层
58 | transition(name='fade')
59 | div.modal.dialog(tabindex='-1' role='dialog' v-if='item !== null')
60 | div.modal-dialog(role='document')
61 | div.modal-content
62 | div.modal-header
63 | h5.modal-title.dialog-title 警告
64 | div.modal-body
65 | p.font12
66 | | 确认要删除
67 | b {{ item.name }}
68 | | (
69 | b {{ item.id }}
70 | |) 吗?
71 | div.modal-footer
72 | button.btn.btn-danger.font12.cur-p(type='button' title='确认' v-on:click='onOkDelete()') 确认
73 | button.btn.btn-secondary.font12.cur-p(type='button' title='取消' data-dismiss="modal" v-on:click='onCancelDelete()') 取消
74 | // 弹出层
75 | transition(name='fade')
76 | div.alert.alert-warning.text-center.alert-body.font12(role='alert' v-if='warnName') 必须输入名称!
77 | transition(name='fade')
78 | div.alert.alert-warning.text-center.alert-body.font12(role='alert' v-if='warnID') 必须输入ID!
79 | transition(name='fade')
80 | div.alert.alert-warning.text-center.alert-body.font12(role='alert' v-if='warnIsReset') 该ID已存在!
81 | script(type='text/javascript' src=require('../../../../.dll/dll'))
--------------------------------------------------------------------------------
/app/src/modules/cut/entry/cut.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | meta(charset='utf-8')
5 | meta(name='viewport' content='width=device-width, initial-scale=1')
6 | link(rel='stylesheet' href=require('bootstrap/dist/css/bootstrap.min.css'))
7 | body
8 | div#vue-app
9 | // 顶部工具栏
10 | header.bg-dark.tools
11 | div
12 | button.btn.btn-primary.mr.font12.cur-p(type='button' title='添加工程' v-on:click='dialogDisplay(true)')
13 | i.icon.icon-film2
14 | | 添加工程
15 | // 剪切列表
16 | div.cutList
17 | table.table.table-bordered.table-hover.cutList-table.font12
18 | thead.bg-primary.cutList-color-fff
19 | tr
20 | th.cutList-w1 视频
21 | th.cutList-w2 时间
22 | th.cutList-w3 操作
23 | tbody(v-if='cutList.length === 0')
24 | tr
25 | td.text-center(colspan='3') 暂无数据
26 | tbody(v-else v-for='(item, index) in cutList')
27 | tr
28 | td
29 | b.cutList-ft From:
30 | | {{ item.video }}
31 | br
32 | b.cutList-ft To:
33 | | {{ item.file }}
34 | td
35 | b.cutList-ft Start:
36 | | {{ item.startHour }} : {{ item.startMinute }} : {{ item.startSecond }}
37 | br
38 | b.cutList-ft End:
39 | | {{ item.endHour }} : {{ item.endMinute }} : {{ item.endSecond }}
40 | td
41 | template(v-if='item.isEnd')
42 | b.mr 剪切完成
43 | template(v-else)
44 | button.btn.btn-sm.btn-outline-danger.mr.font12.cur-p(type='button' title='停止剪切' v-if="'child' in item") 停止剪切
45 | button.btn.btn-sm.btn-outline-dark.mr.font12.cur-p(type='button' title='开始剪切' v-else v-on:click='onStartCut(item)') 开始剪切
46 | button.btn.btn-sm.btn-outline-danger.cutList-disabled.font12(type='button' title='删除' v-on:click='onDelete(item)' v-bind:disabled="'child' in item") 删除
47 | // 弹出层
48 | transition(name='fade')
49 | div.modal.dialog(tabindex='-1' role='dialog' v-if='cutShow')
50 | form.modal-dialog.font12#dialog(role='document')
51 | div.modal-content
52 | div.modal-header
53 | h5.modal-title.dialog-title 添加工程
54 | div.modal-body
55 | div.form-group
56 | label(for='forms-video') 选择视频:
57 | input.form-control-file.font12#forms-video(type='file' v-on:change='onVideoChange($event)')
58 | div.form-group
59 | label.dialog-label 开始时间:
60 | input.form-control.dialog-input.font12#forms-st1(type='text' aria-label='开始时间,时' v-model='startHour')
61 | span.dialog-dian :
62 | input.form-control.dialog-input.font12#forms-st2(type='text' aria-label='开始时间,分' v-model='startMinute')
63 | span.dialog-dian :
64 | input.form-control.dialog-input.font12#forms-st3(type='text' aria-label='开始时间,秒' v-model='startSecond')
65 | div.form-group
66 | label.dialog-label 结束时间:
67 | input.form-control.dialog-input.font12#forms-ed1(type='text' aria-label='结束时间,时' v-model='endHour')
68 | span.dialog-dian :
69 | input.form-control.dialog-input.font12#forms-ed2(type='text' aria-label='结束时间,分' v-model='endMinute')
70 | span.dialog-dian :
71 | input.form-control.dialog-input.font12#forms-ed3(type='text' aria-label='结束时间,秒' v-model='endSecond')
72 | div.form-group
73 | label(for='forms-file') 保存位置(保存成gif格式则导出动态图片):
74 | input.form-control-file.font12#forms-file(type='file' v-bind:nwsaveas='saveas' v-on:change='onFileChange($event)')
75 | div.modal-footer
76 | button.btn.btn-secondary.font12.cur-p(type='button' title='关闭' v-on:click='dialogDisplay(false)') 关闭
77 | button.btn.btn-primary.font12.cur-p(type='button' title='添加' v-on:click='onAddCut()') 添加
78 | transition(name='fade')
79 | div.modal.dialog(tabindex='-1' role='dialog' v-if='item !== null')
80 | div.modal-dialog(role='document')
81 | div.modal-content
82 | div.modal-header
83 | h5.modal-title.dialog-title 警告
84 | div.modal-body
85 | p.font12 确认要删除当前工程吗?
86 | div.modal-footer
87 | button.btn.btn-danger.font12.cur-p(type='button' title='确认' v-on:click='onOkDelete()') 确认
88 | button.btn.btn-secondary.font12.cur-p(type='button' title='取消' data-dismiss="modal" v-on:click='onCancelDelete()') 取消
89 | script(type='text/javascript' src=require('../../../../.dll/dll'))
--------------------------------------------------------------------------------
/app/src/public/icon/fonts/icons.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright bilibiliLiveCatch 段昊辰
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------