IPTV检测功能,作者:buvta

This commit is contained in:
haiyangcui
2020-11-07 23:55:44 +01:00
parent d2263f3f76
commit c3a4cf5cc5
4 changed files with 204 additions and 51 deletions

View File

@@ -31,6 +31,7 @@
"html2canvas": "^1.0.0-rc.7",
"iptv-playlist-parser": "^0.5.0",
"m3u": "0.0.2",
"m3u8-parser": "^4.5.0",
"modern-normalize": "^1.0.0",
"mousetrap": "^1.6.5",
"qrcode.vue": "^1.7.0",

View File

@@ -4,13 +4,15 @@
<el-switch v-model="enableBatchEdit" active-text="批处理分组"></el-switch>
<el-button @click.stop="exportChannels" icon="el-icon-upload2" >导出</el-button>
<el-button @click.stop="importChannels" icon="el-icon-download">导入</el-button>
<el-button @click.stop="removeAllChannels" icon="el-icon-delete-solid">清空</el-button>
<el-button @click="checkAllChannels" icon="el-icon-refresh" :loading="checkAllChannelsLoading">检测{{ this.checkAllChannelsLoading ? this.checkProgress + '/' + this.iptvList.length : '' }}</el-button>
<el-button @click.stop="resetChannelsEvent" icon="el-icon-refresh-left">重置</el-button>
</div>
<div class="listpage-header" id="iptv-header" v-show="enableBatchEdit">
<el-switch v-model="enableBatchEdit" active-text="批处理分组"></el-switch>
<el-input placeholder="新组名" v-model="batchGroupName"></el-input>
<el-switch v-model="batchIsActive" active-text="启用"></el-switch>
<el-button type="primary" icon="el-icon-edit" @click.stop="saveBatchEdit">保存</el-button>
<el-button @click.stop="removeSelectedChannels" icon="el-icon-delete-solid">删除</el-button>
</div>
<div class="listpage-body" id="iptv-table">
<div class="show-table" id="iptv-table">
@@ -19,6 +21,7 @@
size="mini" fit height="100%" row-key="id"
:data="filteredTableData"
@row-click="playEvent"
@select="selectionCellClick"
@selection-change="handleSelectionChange"
@sort-change="handleSortChange">>
<el-table-column
@@ -38,6 +41,20 @@
</el-input>
</template>
</el-table-column>
<el-table-column
prop="isActive"
width="120"
align="center"
:filters = "[{text:'启用', value: 1}, {text:'停用', value: 0}]"
:filter-method="(value, row) => value === row.isActive"
label="启用">
<template slot-scope="scope">
<el-switch
v-model="scope.row.isActive"
@click.native.stop='isActiveChangeEvent(scope.row)'>
</el-switch>
</template>
</el-table-column>
<el-table-column
sort-by="['group', 'name']"
sortable
@@ -45,12 +62,25 @@
prop="group"
label="分组"
:filters="getFilters"
:filter-method="filterHandle"
:filter-method="(value, row) => value === row.group"
filter-placement="bottom-end">
<template slot-scope="scope">
<el-button type="text">{{scope.row.group}}</el-button>
</template>
</el-table-column>
<el-table-column
label="状态"
sortable
:sort-by="['status']"
width="120">
<template slot-scope="scope">
<span v-if="scope.row.status === ' '">
<i class="el-icon-loading"></i>
检测中...
</span>
<span v-else>{{scope.row.status}}</span>
</template>
</el-table-column>
<el-table-column
label="操作"
header-align="right"
@@ -60,6 +90,8 @@
</template>
<template slot-scope="scope">
<el-button @click.stop="moveToTopEvent(scope.row)" type="text">置顶</el-button>
<!-- 检测时先强制批量检测一遍,如果不强制直接单个检测时第一次不会显示“检测中”-->
<el-button size="mini" v-if="iptvList.every(channel => channel.status)" v-show="!checkAllChannelsLoading" @click.stop="checkSingleChannel(scope.row)" type="text">检测</el-button>
<el-button @click.stop="removeEvent(scope.row)" type="text">删除</el-button>
</template>
</el-table-column>
@@ -71,7 +103,8 @@
<script>
import { mapMutations } from 'vuex'
import { iptv, iptvSearch } from '../lib/dexie'
import { iptv as defaultSites } from '../lib/dexie/initData'
import { iptv as defaultChannels } from '../lib/dexie/initData'
import zy from '../lib/site/tools'
import { remote } from 'electron'
import fs from 'fs'
import Sortable from 'sortablejs'
@@ -84,7 +117,14 @@ export default {
searchRecordList: [],
enableBatchEdit: false,
batchGroupName: '',
batchIsActive: true,
shiftDown: false,
selectionBegin: '',
selectionEnd: '',
multipleSelection: [],
checkAllChannelsLoading: false,
checkProgress: 0,
stopFlag: false,
show: {
search: false
}
@@ -136,7 +176,11 @@ export default {
this.getChannels()
}
},
searchTxt () {
enableBatchEdit () {
if (this.checkAllChannelsLoading) {
this.$message.info('正在检测, 请勿操作.')
this.enableBatchEdit = false
}
}
},
methods: {
@@ -144,27 +188,51 @@ export default {
sortByGroup (a, b) {
return a.group.localeCompare(b.group, 'zh')
},
selectionCellClick (selection, row) {
if (this.shiftDown && this.selectionBegin !== '' && selection.includes(row)) {
this.selectionEnd = row.id
const start = Math.min(this.selectionBegin, this.selectionEnd) - 1
const end = Math.max(this.selectionBegin, this.selectionEnd)
const selections = this.iptvList.slice(start, end)
this.$nextTick(() => {
selections.forEach(e => this.$refs.iptvTable.toggleRowSelection(e, true))
})
this.selectionBegin = this.selectionEnd = ''
return
}
if (selection.includes(row)) {
this.selectionBegin = row.id
} else {
this.selectionBegin = ''
}
},
handleSelectionChange (rows) {
this.multipleSelection = rows
},
handleSortChange (column, prop, order) {
this.updateDatabase()
},
saveBatchEdit () {
if (this.multipleSelection && this.batchGroupName) {
this.multipleSelection.forEach(ele => {
ele.group = this.batchGroupName
})
if (this.checkAllChannelsLoading) {
this.$message.info('正在检测, 请勿操作.')
this.enableBatchEdit = false
}
this.updateDatabase()
},
saveBatchEdit () {
this.multipleSelection.forEach(ele => {
if (this.batchGroupName) {
ele.group = this.batchGroupName
}
ele.isActive = this.batchIsActive
})
this.updateDatabase()
},
playEvent (e) {
if (this.checkAllChannelsLoading) {
this.$message.info('正在检测, 请勿操作.')
return false
}
this.video = { iptv: { name: e.name, url: e.url } }
this.view = 'Play'
},
filterHandle (value, row) {
return row.group === value
},
containsearchTxt (i) {
if (this.searchTxt) {
return i.name.toLowerCase().includes(this.searchTxt.toLowerCase())
@@ -173,23 +241,16 @@ export default {
}
},
removeEvent (e) {
if (this.checkAllChannelsLoading) {
this.$message.info('正在检测, 请勿操作.')
return false
}
iptv.remove(e.id).then(res => {
this.getChannels()
}).catch(err => {
this.$message.warning('删除频道失败, 错误信息: ' + err)
})
},
listUpdatedEvent () {
iptv.clear().then(res1 => {
// 重新排序
var id = 1
this.iptvList.forEach(element => {
element.id = id
iptv.add(element)
id += 1
})
})
},
exportChannels () {
const options = {
filters: [
@@ -218,6 +279,10 @@ export default {
})
},
importChannels () {
if (this.checkAllChannelsLoading) {
this.$message.info('正在检测, 请勿操作.')
return false
}
const options = {
filters: [
{ name: 'm3u file', extensions: ['m3u', 'm3u8'] },
@@ -228,7 +293,7 @@ export default {
remote.dialog.showOpenDialog(options).then(result => {
if (!result.canceled) {
var docs = this.iptvList
var id = docs.length
var id = docs.length + 1
result.filePaths.forEach(file => {
if (file.endsWith('m3u') || file.endsWith('m3u8')) {
const parser = require('iptv-playlist-parser')
@@ -240,6 +305,7 @@ export default {
id: id,
name: ele.name,
url: ele.url,
isActive: true,
group: this.determineGroup(ele.name)
}
id += 1
@@ -256,6 +322,7 @@ export default {
id: id,
name: ele.name,
url: ele.url,
isActive: ele.isActive === undefined ? true : ele.isActive,
group: this.determineGroup(ele.name)
}
id += 1
@@ -265,9 +332,9 @@ export default {
}
})
// 获取name不重复的列表
const uniqueList = [...new Map(docs.map(item => [item.name, item])).values()]
// const uniqueList = [...new Map(docs.map(item => [item.name, item])).values()]
iptv.clear().then(res => {
iptv.bulkAdd(uniqueList).then(e => {
iptv.bulkAdd(docs).then(e => { // 支持导入同名频道,群里反馈
this.getChannels()
this.$message.success('导入成功')
})
@@ -291,19 +358,27 @@ export default {
}
},
resetChannelsEvent () {
this.resetChannels(defaultSites)
this.stopFlag = true
if (this.checkAllChannelsLoading) {
this.$message.info('部分检测还未完全终止, 请稍等...')
return
}
iptv.clear().then(iptv.bulkAdd(defaultChannels).then(this.getChannels()))
},
resetChannels (newSites) {
this.resetId(newSites)
iptv.clear().then(iptv.bulkAdd(newSites).then(this.getChannels()))
},
removeAllChannels () {
iptv.clear().then(res => {
this.getChannels()
})
removeSelectedChannels () {
this.multipleSelection.forEach(e => iptv.remove(e.id))
this.$refs.iptvTable.clearFilter()
this.getChannels()
this.updateDatabase()
this.enableBatchEdit = false
},
getChannels () {
iptv.all().then(res => {
res.forEach(ele => {
if (ele.isActive === undefined) {
ele.isActive = true
}
})
this.iptvList = res
})
},
@@ -330,11 +405,15 @@ export default {
}
},
moveToTopEvent (i) {
if (this.checkAllChannelsLoading) {
this.$message.info('正在检测, 请勿操作.')
return false
}
this.iptvList.sort(function (x, y) { return (x.name === i.name && x.url === i.url) ? -1 : (y.name === i.name && y.url === i.url) ? 1 : 0 })
this.updateDatabase()
},
syncTableData () {
if (this.$refs.iptvTable.tableData && this.$refs.iptvTable.tableData.length === this.iptvList.length) {
if (this.$refs.iptvTable.tableData) {
this.iptvList = this.$refs.iptvTable.tableData
}
},
@@ -353,6 +432,10 @@ export default {
})
},
rowDrop () {
if (this.checkAllChannelsLoading) {
this.$message.info('正在检测, 请勿操作.')
return false
}
const tbody = document.getElementById('iptv-table').querySelector('.el-table__body-wrapper tbody')
const _this = this
Sortable.create(tbody, {
@@ -362,10 +445,64 @@ export default {
_this.updateDatabase()
}
})
},
isActiveChangeEvent (row) {
iptv.remove(row.id)
iptv.add(row)
},
async checkAllChannels () {
this.checkAllChannelsLoading = true
this.stopFlag = false
this.checkProgress = 0
const uncheckedList = this.iptvList.filter(e => e.status === undefined || e.status === ' ') // 未检测过的优先
const other = this.iptvList.filter(e => !uncheckedList.includes(e))
await this.checkChannelList(uncheckedList)
await this.checkChannelList(other).then(res => {
this.checkAllChannelsLoading = false
this.getChannels()
})
},
async checkChannelList (channelList) {
var siteList = {}
channelList.forEach(channel => {
const site = channel.url.split('/')[2]
if (siteList[site]) {
siteList[site].push(channel)
} else {
siteList[site] = [channel]
}
})
await Promise.all(Object.values(siteList).map(site => this.checkSingleSite(site)))
},
async checkSingleSite (channelArray) {
for (const c of channelArray) {
if (this.stopFlag) return false
await this.checkSingleChannel(c)
}
},
async checkSingleChannel (row) {
row.status = ' '
if (this.stopFlag) {
this.checkProgress += 1
return row.status
}
const flag = await zy.checkChannel(row.url)
this.checkProgress += 1
if (flag) {
row.status = '可用'
} else {
row.status = '失效'
row.isActive = false
}
iptv.remove(row.id)
iptv.add(row)
return row.status
}
},
mounted () {
this.rowDrop()
addEventListener('keydown', code => { if (code.keyCode === 16) this.shiftDown = true })
addEventListener('keyup', code => { if (code.keyCode === 16) this.shiftDown = false })
},
created () {
this.getChannels()

View File

@@ -6,14 +6,14 @@ const db = new Dexie('zy')
db.version(4).stores({
search: '++id, keywords',
iptvSearch: '++id, keywords',
setting: 'id, theme, site, shortcut, view, externalPlayer, searchAllSites, excludeRootClasses, excludeR18Films, forwardTimeInSec, starViewMode, recommendationViewMode, password',
setting: 'id, theme, site, shortcut, view, externalPlayer, searchAllSites, excludeRootClasses, excludeR18Films, forwardTimeInSec, starViewMode, recommendationViewMode, historyViewMode, password',
shortcut: 'name, key, desc',
star: '++id, [key+ids], site, name, detail, index, rate, hasUpdate',
recommendation: '++id, [key+ids], site, name, detail, index, rate, hasUpdate',
sites: '++id, key, name, api, download, isActive, group',
history: '++id, [site+ids], name, type, year, index, time, detail',
history: '++id, [site+ids], name, type, year, index, time, duration, detail',
mini: 'id, site, ids, name, index, time',
iptv: '++id, name, url, group'
iptv: '++id, name, url, group, isActive'
})
db.on('populate', () => {

View File

@@ -2,6 +2,8 @@ import { sites } from '../dexie'
import axios from 'axios'
import parser from 'fast-xml-parser'
import cheerio from 'cheerio'
import { Parser as M3u8Parser } from 'm3u8-parser'
// 请求超时时限
axios.defaults.timeout = 5000
@@ -12,16 +14,6 @@ axios.defaults.retry = 2
// 请求的间隙
axios.defaults.retryDelay = 1000
// 添加请求拦截器(配置发送请求的信息)
axios.interceptors.request.use(function (config) {
// 处理请求之前的配置
// 引入代理,播放器代理怎么搞?
return config
}, function (error) {
// 请求失败的处理
return Promise.reject(error)
})
// 添加响应拦截器
axios.interceptors.response.use(function (response) {
// 对响应数据做些事
@@ -282,6 +274,29 @@ const zy = {
return false
}
},
/**
* 检查直播源
* @param {*} channel 直播频道 url
* @returns boolean
*/
async checkChannel (channel) {
return new Promise((resolve, reject) => {
axios.get(channel).then(res => {
const manifest = res.data
var parser = new M3u8Parser()
parser.push(manifest)
parser.end()
var parsedManifest = parser.manifest
if (parsedManifest.segments.length) {
resolve(true)
} else {
resolve(false)
}
}).catch(e => {
resolve(false)
})
})
},
/**
* 获取豆瓣页面链接
* @param {*} name 视频名称