├── 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 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------