Compare commits

...

112 Commits

Author SHA1 Message Date
buvta
2c93d755c7 Revert "修复play-request-was-interrupted报错",重新发布
This reverts commit 088cd70e41.
2021-01-21 18:16:25 +08:00
buvta
fe75084dd0 v2.7.2 2021-01-21 17:19:39 +08:00
buvta
047224ce80 修复直播时触发timeupdate导致的异常 2021-01-21 17:11:46 +08:00
buvta
31ea52e267 为应对flv切换到其它格式时可能出现的黑屏有声音情况,新增的关闭按钮常显 2021-01-21 16:11:52 +08:00
buvta
088cd70e41 修复play-request-was-interrupted报错 2021-01-21 15:47:02 +08:00
buvta
6bfd96942d 切换频道首尾部自动轮替 2021-01-21 15:44:28 +08:00
buvta
5d6579326c 频道所有源全部卡顿时自动换台 2021-01-21 15:36:33 +08:00
buvta
334933ce82 直播卡顿时自动换源 2021-01-20 23:22:23 +08:00
buvta
9fd3e5e2ed 修复flv直播停止功能 2021-01-20 22:22:46 +08:00
buvta
1e7199af3f 修复改动flv直播状态导致的bug 2021-01-20 18:23:10 +08:00
buvta
0d83b277e7 下移播放器速率栏,避免鼠标移动时误触进度条 2021-01-19 23:34:12 +08:00
buvta
08cbd1a73c 修正某些源站剧集顺序 2021-01-19 22:34:50 +08:00
buvta
01e58f458b 修复zy.download因分隔符导致的bug 2021-01-19 20:26:53 +08:00
buvta
3ac21ad1e5 更新7k源解析接口地址 2021-01-19 19:54:40 +08:00
buvta
f098198448 可通过重置更新应用默认解析接口地址 2021-01-19 19:54:07 +08:00
buvta
35e6e59b73 添加应用默认解析接口设置 2021-01-19 17:33:39 +08:00
buvta
bf9eaf09eb 调整解析接口默认打开方式 2021-01-19 17:28:55 +08:00
buvta
03ec267c1f 修改jiexiURL应对源自带解析的情况 2021-01-19 16:29:07 +08:00
buvta
97aea7b98d 修复因分隔符"$"改动引入的bug 2021-01-19 16:22:02 +08:00
buvta
f8ad00b4d0 zy.detail获取到的链接分隔符包含多个"$", 88资源站有点怪 2021-01-18 22:30:03 +08:00
buvta
607d4baae4 修复使图片无法加载时分享可用 2021-01-18 21:07:57 +08:00
buvta
51208892d0 修复zy.detail获取到的链接不包含"$"时产生的bug 2021-01-18 21:07:42 +08:00
buvta
516b791719 再次调整直播源检测跳过功能 2021-01-18 18:44:29 +08:00
buvta
8032645c25 修复调整生成调试信息时引入的bug 2021-01-18 18:33:15 +08:00
Hunlongyu
9d0536a3f8 升级依赖, 解决 GitHub 报错问题 2021-01-18 10:38:01 +08:00
Hunlongyu
85a39d67ce 修改第三方播放路径为 Gitee. 2021-01-18 10:28:01 +08:00
Hunlongyu
3184147910 升级依赖, 修复分享图片和二维码失效的问题 2021-01-18 10:09:18 +08:00
buvta
9d8a09e90d 修复直播源检测跳过功能 2021-01-17 23:27:56 +08:00
buvta
5d08e715aa 使用代理时可检测flv直播源 2021-01-17 14:55:39 +08:00
buvta
98378788fd 直播源检测支持flv格式,使用代理时暂不可用 2021-01-16 17:20:19 +08:00
buvta
af768d527a 直播支持flv格式,播放器停止功能待修复 2021-01-15 22:12:55 +08:00
buvta
4e5aab6f66 调整播放调试生成信息 2021-01-03 22:49:17 +08:00
buvta
fda305a8ae Film搜索亦过滤 2021-01-03 00:14:18 +08:00
buvta
21487e2754 Film添加列表时过滤掉无链接的项 2021-01-02 23:58:00 +08:00
buvta
0f934413d0 再次调整axios超时设置 2021-01-02 21:38:56 +08:00
buvta
96c68da8b7 有些直播源网址路径不含m3u8而是以参数带m3u8结尾 2021-01-02 21:15:17 +08:00
buvta
0e0de8f23c 修正关闭内嵌播放器bug 2021-01-02 16:01:30 +08:00
buvta
2b24ac7d0c 手动修正变量定义var 2021-01-02 15:05:24 +08:00
buvta
458144a6ea 修正清理缓存描述 2021-01-02 14:58:24 +08:00
buvta
69113c7a9a 更新下载完毕后通知 2021-01-01 22:55:14 +08:00
buvta
d247a2fa23 分享在详情页可选定集数,播放页面分享为当前集,其它页面分享默认首集 2021-01-01 22:50:33 +08:00
buvta
c14c78b68f Star添加DetailCache 2021-01-01 15:46:54 +08:00
buvta
cc4e2c5cad 历史页面禁止拖动 2021-01-01 15:24:24 +08:00
buvta
073e111af8 嵌入播放时添加关闭按钮 2021-01-01 15:19:24 +08:00
buvta
fea076318d v2.7.1 2020-12-31 23:30:00 +08:00
buvta
df867fa919 修复播放和历史页面无法分享的bug 2020-12-31 23:27:25 +08:00
buvta
a7f0030462 疑似需要授权,先移除阿里云源 2020-12-31 22:53:05 +08:00
buvta
b5274c79b0 修复获取下载链接时的bug 2020-12-31 22:32:59 +08:00
buvta
a2de1984e3 改为原来的更新组件,避免因为超时而中断卡住 2020-12-31 22:12:30 +08:00
buvta
81b5391f64 详情页根据历史记录更新videoFlag 2020-12-31 20:20:34 +08:00
buvta
2c6ad44974 根据videoFlag获取下载链接 2020-12-31 20:11:06 +08:00
buvta
e1c942dc7b 过滤掉无效链接 2020-12-31 19:16:35 +08:00
buvta
4420e6961e 修复因为删除m3u8List数据库更新bug 2020-12-31 18:35:25 +08:00
buvta
2deffab3ba 解决detail丢失问题 2020-12-31 17:08:55 +08:00
buvta
05a07bee62 移除m3u8List 2020-12-31 16:34:18 +08:00
buvta
025d1606f7 保存videoFlag 2020-12-31 16:34:18 +08:00
buvta
b72c88dfb9 将ZY支持的播放列表前置到首位 2020-12-31 16:34:05 +08:00
buvta
db240cc163 优化getPlayer 2020-12-31 13:38:44 +08:00
haiyangcui
0ef55f0139 在线解析的视频,不显示"手动跳略时长" 2020-12-30 17:14:46 +01:00
haiyangcui
0ad9b49224 解决有多个视频列表时的播放错误 2020-12-30 17:08:21 +01:00
haiyangcui
ccbf4fb5d7 移除不必要的log 2020-12-30 17:03:47 +01:00
haiyangcui
46e376a351 翻转需要解析的视频网站的视频顺序,保证最近更新的视频在最上面 2020-12-30 16:58:33 +01:00
buvta
23e693a314 多个播放视频列表时优先选择应用支持的 2020-12-30 23:52:38 +08:00
buvta
3a3e37f4bd 修复直接手动设置调略设置时片头片尾一起变动 2020-12-30 23:08:46 +08:00
buvta
e568053b95 修复改动导致快捷键设置片头片尾标记失效 2020-12-30 23:02:22 +08:00
haiyangcui
fe9cd9f5bc 无需记录mp4List 2020-12-30 15:44:19 +01:00
buvta
8cf3ec94d9 清理Play多余代码 2020-12-30 20:23:57 +08:00
buvta
112f92d3e0 在线解析时添加进历史记录 2020-12-30 20:10:11 +08:00
buvta
88db766337 添加提示,避免调用解析接口响应慢时尴尬空白 2020-12-30 19:32:55 +08:00
buvta
205b9d7979 改写zy.download以优化代码 2020-12-30 17:13:12 +08:00
buvta
d5a7771a67 vuex引入DetailCache,避免重复获取详情页 2020-12-30 15:57:08 +08:00
buvta
655ed7fb16 解决改动新引入的bug 2020-12-30 12:41:34 +08:00
haiyangcui
808f3d2012 修复播放完在线解析的视频后无法播放m3u8的问题 2020-12-29 23:38:02 +01:00
haiyangcui
ee4b944abf 修改被选中的video flag背景色 2020-12-29 23:03:47 +01:00
haiyangcui
b4abd9369f 将视频flag列表和剧集列表分开 2020-12-29 22:56:08 +01:00
haiyangcui
079da637ec 只有当视频有多个源时,显示视频源flag列表 2020-12-29 22:53:30 +01:00
haiyangcui
0c8a100d1c 视频分类被选后时改变背景颜色 2020-12-29 22:29:39 +01:00
haiyangcui
0cc330463f 获取全部视频提供的视频列表,为兼容目的,zy.detail函数仍返回m3u8List和mp4List, 会在后续工作中陆续删除 2020-12-29 22:04:52 +01:00
buvta
f50d669a5f 修正下载功能并调整通知描述 2020-12-30 00:16:22 +08:00
buvta
57fd1d325e 修复Play点击播放器下方按钮时弹窗反复弹出及按钮active状态 2020-12-29 22:59:48 +08:00
buvta
e31b921486 修复Play点播m3u8播放列表导出功能 2020-12-29 21:29:10 +08:00
buvta
08b39a5bb1 调整Play历史记录的播放时间保存 2020-12-29 18:30:05 +08:00
buvta
1fefc792fc 修改请求referer 2020-12-29 17:03:13 +08:00
buvta
b9a4784de2 bug依旧,xgplayer-mp4似乎没啥作用先移除 2020-12-29 15:59:43 +08:00
buvta
855472fe68 修复mp4停止后点击播放按钮无法播放,并引入xgplayer-flv.js 2020-12-29 00:50:59 +08:00
buvta
4834d4423c Revert "eslint自动修复var定义变量带来的警告"
This reverts commit 4de2177cc7.
2020-12-28 23:05:12 +08:00
buvta
4de2177cc7 eslint自动修复var定义变量带来的警告 2020-12-28 22:29:11 +08:00
buvta
ca9f9c9160 修复点击播放按钮时产生重复历史记录 2020-12-28 21:37:29 +08:00
buvta
7c0f688f9d 修正Play页面最近历史记录 2020-12-28 20:15:09 +08:00
buvta
4bfae63201 点播支持mp4格式视频源 2020-12-28 19:45:18 +08:00
haiyangcui
419a56518f 解决上次的commit错误 2020-12-27 21:55:30 +01:00
haiyangcui
36efdcd869 sites数据引入jiexiUrl数据段,播放时需要解析时,不同源可以定义不同的解析网站 2020-12-27 21:53:24 +01:00
haiyangcui
a297aca812 直接在内嵌iframe中播放阿里云资源 2020-12-27 17:36:49 +01:00
buvta
0b4dd2e859 阿里云资源站临时可用 2020-12-28 00:00:24 +08:00
buvta
3142306a0c 优化tools使某些源站api可用 2020-12-27 21:20:28 +08:00
haiyangcui
9d4765c1ea 修改拼写错误 2020-12-26 22:59:25 +01:00
buvta
547dffb922 动态配置axios超时 2020-12-26 22:35:30 +08:00
buvta
84e61acde7 Update bug.md 2020-12-26 14:18:10 +08:00
haiyangcui
11c756466c 播放的一开始就先清空onlineUrl,否则播放iptv会有问题 2020-12-24 16:22:51 +01:00
haiyangcui
06291ffc4b 解决播放解析视频后再打开,因为没有时间记录而报错的问题 2020-12-24 16:06:56 +01:00
haiyangcui
1f65e5ba0a 在播放页面利用iframe嵌入需要在线解析的内容 2020-12-24 15:53:02 +01:00
haiyangcui
392f0fd326 改进豆瓣搜索,匹配名字和年代 2020-12-24 13:04:01 +01:00
haiyangcui
a6b6407679 采用7K接口 2020-12-23 14:30:59 +01:00
haiyangcui
96678bf5df 只有当xg-btn-showhistory存在时,才设置它的样式 2020-12-23 14:30:05 +01:00
haiyangcui
3c37b8286f 解决运行时因为star history table还没实例化而出现的错误 2020-12-23 14:27:53 +01:00
haiyangcui
38862f6ad8 升级依赖,并解决升级后出现的错误 2020-12-23 14:24:43 +01:00
buvta
35213238f4 使用el内置clickoutside替代vue-clickaway,修复点击菜单按钮时被捕捉失效 2020-12-21 21:01:51 +08:00
buvta
2a03388d6b v2.7.0 2020-12-19 13:29:31 +08:00
buvta
ef704a9d45 禁用allowRunningInsecureContent 2020-12-19 13:27:07 +08:00
buvta
946978fa86 修复深色主题下el-switch标签文本不可见 2020-12-19 13:10:30 +08:00
haiyangcui
70b03e67fb 在右侧显示分隔栏内容,1.在中间的时候永远无法和中间的下拉菜单对齐, 2. 不常用的按钮放在边侧,以降低对用户注意力的影响 2020-12-18 22:25:55 +01:00
haiyangcui
1ee7c6f032 设置不同主题下的工具栏样式 2020-12-18 22:11:18 +01:00
26 changed files with 1537 additions and 965 deletions

View File

@@ -12,6 +12,7 @@ module.exports = {
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'standard/no-callback-literal': 0
}
}

View File

@@ -1,7 +1,7 @@
---
name: 报告Bug(请先查看常见问题及搜索issue列表中有无你要提的问题)
name: 报告Bug(请先查看常见问题及搜索已关闭issue列表中有无你要提的问题)
about: 创建报告以帮助我们改进
title: '(未回答的问题请删除)'
title: '(未回答的问题请删除,减少多余信息干扰'
labels: bug
assignees: ''

View File

@@ -1,6 +1,6 @@
{
"name": "zy",
"version": "2.6.10",
"version": "2.7.2",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
@@ -13,58 +13,58 @@
"postuninstall": "electron-builder install-app-deps",
"electron:generate-icons": "electron-icon-builder --input=./public/icon.png --output=build --flatten",
"release": "vue-cli-service electron:build -p always",
"dist": "vue-cli-service electron:build --win --x64"
"dist": "vue-cli-service electron:build --win --ia32"
},
"main": "background.js",
"dependencies": {
"@imjs/electron-differential-updater": "^5.1.3",
"axios": "^0.21.0",
"cheerio": "^1.0.0-rc.3",
"core-js": "^3.8.0",
"axios": "^0.21.1",
"cheerio": "^1.0.0-rc.5",
"core-js": "^3.8.2",
"dexie": "^3.0.3",
"electron-localshortcut": "^3.2.1",
"electron-proxy-agent": "^1.2.0",
"element-ui": "^2.14.1",
"fast-xml-parser": "^3.17.4",
"electron-updater": "^4.3.5",
"element-ui": "^2.15.0",
"fast-xml-parser": "^3.17.6",
"html2canvas": "^1.0.0-rc.7",
"iptv-playlist-parser": "^0.5.0",
"iptv-playlist-parser": "^0.5.4",
"m3u": "0.0.2",
"m3u8-parser": "^4.5.0",
"m3u8-parser": "^4.5.2",
"memcached": "^2.2.2",
"modern-normalize": "^1.0.0",
"mousetrap": "^1.6.5",
"pinyin-match": "^1.1.1",
"pinyin-match": "^1.1.7",
"qrcode.vue": "^1.7.0",
"randomstring": "^1.1.5",
"session": "^0.1.0",
"sortablejs": "^1.12.0",
"sortablejs": "^1.13.0",
"v-fit-columns": "^0.2.0",
"vue": "^2.6.12",
"vue-clickaway": "^2.2.2",
"vue-infinite-loading": "^2.4.5",
"vue-waterfall-plugin": "^1.1.0",
"vuedraggable": "^2.24.3",
"vuex": "^3.6.0",
"xgplayer": "^2.13.2",
"xgplayer-hls.js": "^2.2.5"
"xgplayer": "^2.16.0",
"xgplayer-flv.js": "^2.1.2",
"xgplayer-hls.js": "^2.3.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.9",
"@vue/cli-plugin-eslint": "~4.5.9",
"@vue/cli-plugin-vuex": "~4.5.9",
"@vue/cli-service": "~4.5.9",
"@vue/eslint-config-standard": "^5.1.2",
"@vue/cli-plugin-babel": "~4.5.10",
"@vue/cli-plugin-eslint": "~4.5.10",
"@vue/cli-plugin-vuex": "~4.5.10",
"@vue/cli-service": "~4.5.10",
"@vue/eslint-config-standard": "^6.0.0",
"babel-eslint": "^10.1.0",
"babel-plugin-component": "^1.1.1",
"electron": "^11.0.3",
"electron": "^11.1.1",
"electron-devtools-installer": "^3.1.1",
"eslint": "^7.14.0",
"eslint": "^7.16.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.1.0",
"eslint-plugin-vue": "^7.1.0",
"sass": "^1.29.0",
"eslint-plugin-vue": "^7.3.0",
"sass": "^1.30.0",
"sass-loader": "^10.1.0",
"vue-cli-plugin-electron-builder": "2.0.0-rc.5",
"vue-template-compiler": "^2.6.12"

View File

@@ -4,6 +4,7 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta name="referrer" content="same-origin"/>
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>

View File

@@ -131,6 +131,12 @@
border-color: var(--d-c-8);
}
}
.selected {
background-color: var(--d-c-5);
&:hover{
background-color: var(--d-c-5);
}
}
}
}
}
@@ -139,6 +145,12 @@
.play{
background-color: var(--d-bgc-1);
box-shadow: var(--d-bsc);
.el-switch__label {
color: var(--d-fc-2)
}
.el-switch__label.is-active {
color: #409EFF
}
.el-input{
input{
background-color: var(--d-bgc-1);
@@ -155,6 +167,21 @@
}
}
.box{
.title{
.right{
color: var(--d-fc-2);
svg{
stroke: var(--d-c-5);
stroke-width: 1;
fill: none;
&:hover{
stroke: var(--d-c-8);
stroke-width: 1.5;
fill: var(--d-c-2);
}
}
}
}
.more{
span{
svg{
@@ -319,6 +346,19 @@
// Page of list using table and picture
.listpage{
color: var(--d-fc-2);
.listpage-header-divider{
background-color: var(--d-bgc-1);
.el-divider__text {
background-color: var(--d-bgc-2);
}
.el-button{
background-color: var(--d-bgc-2);
color: var(--d-fc-2);
&:hover{
color: var(--d-fc-3)
}
}
}
.listpage-header, .toolbar{
border-bottom-color: var(--d-c-3);
.btn{
@@ -326,6 +366,12 @@
color: var(--d-fc-3)
}
}
.el-switch__label {
color: var(--d-fc-2)
}
.el-switch__label.is-active {
color: #409EFF
}
.el-button{
background-color: var(--d-bgc-2);
color: var(--d-fc-2);

View File

@@ -131,6 +131,12 @@
border-color: var(--g-c-8);
}
}
.selected {
background-color: var(--g-c-5);
&:hover{
background-color: var(--g-c-5);
}
}
}
}
}
@@ -155,6 +161,21 @@
}
}
.box{
.title{
.right{
color: var(--g-fc-2);
svg{
stroke: var(--g-c-5);
stroke-width: 1;
fill: none;
&:hover{
stroke: var(--g-c-8);
stroke-width: 1.5;
fill: var(--g-c-2);
}
}
}
}
.more{
span{
svg{
@@ -319,6 +340,19 @@
// Page of list using table and picture
.listpage{
color: var(--g-fc-2);
.listpage-header-divider{
background-color: var(--g-bgc-1);
.el-divider__text {
background-color: var(--g-bgc-2);
}
.el-button{
background-color: var(--g-bgc-2);
color: var(--g-fc-2);
&:hover{
color: var(--g-fc-3)
}
}
}
.listpage-header, .toolbar{
border-bottom-color: var(--g-c-3);
.btn{

View File

@@ -131,6 +131,12 @@
border-color: var(--l-c-8);
}
}
.selected {
background-color: var(--l-c-5);
&:hover{
background-color: var(--l-c-5);
}
}
}
}
}
@@ -155,6 +161,21 @@
}
}
.box{
.title{
.right{
color: var(--l-fc-2);
svg{
stroke: var(--l-c-5);
stroke-width: 1;
fill: none;
&:hover{
stroke: var(--l-c-8);
stroke-width: 1.5;
fill: var(--l-c-2);
}
}
}
}
.more{
span{
svg{
@@ -319,6 +340,19 @@
// Page of list using table and picture
.listpage{
color: var(--l-fc-2);
.listpage-header-divider{
background-color: var(--l-bgc-1);
.el-divider__text {
background-color: var(--l-bgc-2);
}
.el-button{
background-color: var(--l-bgc-2);
color: var(--l-fc-2);
&:hover{
color: var(--l-fc-3)
}
}
}
.listpage-header, .toolbar{
border-bottom-color: var(--l-c-3);
.btn{

View File

@@ -131,6 +131,12 @@
border-color: var(--p-c-8);
}
}
.selected {
background-color: var(--p-c-5);
&:hover{
background-color: var(--p-c-5);
}
}
}
}
}
@@ -155,6 +161,21 @@
}
}
.box{
.title{
.right{
color: var(--p-fc-2);
svg{
stroke: var(--p-c-5);
stroke-width: 1;
fill: none;
&:hover{
stroke: var(--p-c-8);
stroke-width: 1.5;
fill: var(--p-c-2);
}
}
}
}
.more{
span{
svg{
@@ -319,6 +340,19 @@
// Page of list using table and picture
.listpage{
color: var(--p-fc-2);
.listpage-header-divider{
background-color: var(--p-bgc-1);
.el-divider__text {
background-color: var(--p-bgc-2);
}
.el-button{
background-color: var(--p-bgc-2);
color: var(--p-fc-2);
&:hover{
color: var(--p-fc-3)
}
}
}
.listpage-header, .toolbar{
border-bottom-color: var(--p-c-3);
.btn{

View File

@@ -5,6 +5,7 @@ import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'
import { initUpdater } from './lib/update/update'
const isDevelopment = process.env.NODE_ENV !== 'production'
// const log = require('electron-log') // 用于调试主程序
app.commandLine.appendSwitch('disable-features', 'OutOfBlinkCors') // 允许跨域
app.commandLine.appendSwitch('--ignore-certificate-errors', 'true') // 忽略证书相关错误
@@ -22,7 +23,8 @@ function createWindow () {
webPreferences: {
webSecurity: false,
enableRemoteModule: true,
nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION
nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
allowRunningInsecureContent: false
}
})
@@ -34,6 +36,23 @@ function createWindow () {
win.loadURL('app://./index.html')
}
// 修改request headers
// Sec-Fetch下禁止修改浏览器自动加上请求头 https://www.cnblogs.com/fulu/p/13879080.html 暂时先用index.html的meta referer policy替代
const filter = {
urls: ['http://*/*', 'http://*/*']
}
win.webContents.session.webRequest.onBeforeSendHeaders(filter, (details, callback) => {
const url = new URL(details.url)
details.requestHeaders.Origin = url.origin
if (!details.url.includes('//localhost') && details.requestHeaders.Referer && details.requestHeaders.Referer.includes('//localhost')) {
details.requestHeaders.Referer = url.origin
}
callback({
cancel: false,
requestHeaders: details.requestHeaders
})
})
initUpdater(win)
win.on('closed', () => {

View File

@@ -36,7 +36,7 @@
</div>
</div>
<div class="operate">
<span @click="playEvent(0)">播放</span>
<span @click="playEvent(selectedEpisode)">播放</span>
<span @click="starEvent">收藏</span>
<span @click="downloadEvent">下载</span>
<span @click="shareEvent">分享</span>
@@ -52,10 +52,16 @@
</span>
</div>
<div
class="desc" v-show="info.des">{{info.des}}</div>
class="desc" v-show="info.des">{{info.des}}
</div>
<div class="m3u8" v-if="videoFullList.length > 1">
<div class="box">
<span v-bind:class="{ selected: i.flag === videoFlag }" v-for="(i, j) in videoFullList" :key="j" @click="updateVideoList(i)">{{i.flag}}</span>
</div>
</div>
<div class="m3u8">
<div class="box">
<span v-for="(i, j) in m3u8List" :key="j" @click="playEvent(j)">{{i | ftName}}</span>
<span v-bind:class="{ selected: j === selectedEpisode }" v-for="(i, j) in videoList" :key="j" @click="playEvent(j)" @mouseenter="() => { selectedEpisode = j }">{{ i | ftName(j) }}</span>
</div>
</div>
</div>
@@ -76,17 +82,24 @@ export default {
data () {
return {
loading: true,
m3u8List: [],
videoFlag: '',
videoList: [],
videoFullList: [],
info: {},
playOnline: false,
selectedEpisode: 0, // 选定集数
selectedOnlineSite: '哔嘀',
onlineSites: ['哔嘀', '素白白', '简影', '极品', '喜欢看', '1080影视']
}
},
filters: {
ftName (e) {
const name = e.split('$')[0]
return name
ftName (e, n) {
const num = e.split('$')
if (num.length > 1) {
return e.split('$')[0]
} else {
return `${(n + 1)}`
}
}
},
computed: {
@@ -121,26 +134,52 @@ export default {
set (val) {
this.SET_SHARE(val)
}
},
DetailCache: {
get () {
return this.$store.getters.getDetailCache
},
set (val) {
this.SET_DetailCache(val)
}
}
},
methods: {
...mapMutations(['SET_VIEW', 'SET_VIDEO', 'SET_DETAIL', 'SET_SHARE']),
...mapMutations(['SET_VIEW', 'SET_VIDEO', 'SET_DETAIL', 'SET_SHARE', 'SET_DetailCache']),
addClass (flag) {
if (flag === this.videoFlag) {
return 'selectedBox'
} else {
return 'box'
}
},
close () {
this.detail.show = false
},
async updateVideoList (e) {
this.videoFlag = e.flag
this.videoList = e.list
const db = await history.find({ site: this.detail.key, ids: this.detail.info.id })
if (db) {
const doc = { ...db }
doc.videoFlag = e.flag
delete doc.id
history.update(db.id, doc)
}
},
async playEvent (n) {
if (!this.playOnline) {
const db = await history.find({ site: this.detail.key, ids: this.detail.info.id })
if (db) {
this.video = { key: db.site, info: { id: db.ids, name: db.name, index: n, site: this.detail.site } }
this.video = { key: db.site, info: { id: db.ids, name: db.name, index: n, site: this.detail.site, videoFlag: this.videoFlag } }
} else {
this.video = { key: this.detail.key, info: { id: this.detail.info.id, name: this.detail.info.name, index: n, site: this.detail.site } }
this.video = { key: this.detail.key, info: { id: this.detail.info.id, name: this.detail.info.name, index: n, site: this.detail.site, videoFlag: this.videoFlag } }
}
this.video.detail = this.info
this.view = 'Play'
this.detail.show = false
} else {
const db = await history.find({ site: this.detail.key, ids: this.detail.info.id })
const db = await history.find({ site: this.detail.key, ids: this.info.id })
if (db) {
db.index = n
db.detail = this.info
@@ -211,63 +250,58 @@ export default {
}
},
downloadEvent () {
const key = this.detail.key
const id = this.info.id
zy.download(key, id).then(res => {
if (res && res.m3u8List) {
const list = res.m3u8List.split('#')
let downloadUrl = ''
for (const i of list) {
const url = encodeURI(i.split('$')[1])
downloadUrl += (url + '\n')
}
clipboard.writeText(downloadUrl)
this.$message.success('『MP4』格式的链接已复制, 快去下载吧!')
} else {
zy.detail(key, id).then(res => {
const list = [...res.m3u8List]
let downloadUrl = ''
for (const i of list) {
const url = encodeURI(i.split('$')[1])
downloadUrl += (url + '\n')
}
clipboard.writeText(downloadUrl)
this.$message.success('『M3U8』格式的链接已复制, 快去下载吧!')
})
}
zy.download(this.detail.key, this.info.id, this.videoFlag).then(res => {
clipboard.writeText(res.downloadUrls)
this.$message.success(res.info)
}).catch((err) => {
this.$message.error(err.info)
})
},
shareEvent () {
this.share = {
show: true,
key: this.detail.key,
info: this.detail.info
info: this.info,
index: this.selectedEpisode
}
},
doubanLinkEvent () {
const name = this.detail.info.name.trim()
zy.doubanLink(name).then(link => {
const name = this.info.name.trim()
const year = this.info.year
zy.doubanLink(name, year).then(link => {
const open = require('open')
open(link)
})
},
getDoubanRate () {
const name = this.detail.info.name.trim()
zy.doubanRate(name).then(res => {
this.info.rate = res
})
async getDoubanRate () {
const name = this.info.name.trim()
const year = this.info.year
this.info.rate = await zy.doubanRate(name, year)
},
getDetailInfo () {
async getDetailInfo () {
const id = this.detail.info.ids || this.detail.info.id
zy.detail(this.detail.key, id).then(res => {
if (res) {
this.info = res
this.$set(this.info, 'rate', '')
this.m3u8List = res.m3u8List
this.getDoubanRate()
this.loading = false
const cacheKey = this.detail.key + '@' + id
const db = await history.find({ site: this.detail.key, ids: id })
if (db) {
this.videoFlag = db.videoFlag
this.selectedEpisode = db.index
}
if (!this.DetailCache[cacheKey]) {
this.DetailCache[cacheKey] = await zy.detail(this.detail.key, id)
}
const res = this.DetailCache[cacheKey]
if (res) {
this.info = res
this.$set(this.info, 'rate', this.DetailCache[cacheKey].rate || '')
this.videoFlag = this.videoFlag || res.fullList[0].flag
this.videoList = res.fullList[0].list
this.videoFullList = res.fullList
this.loading = false
if (!this.info.rate) {
await this.getDoubanRate()
this.DetailCache[cacheKey].rate = this.info.rate
}
})
}
}
},
created () {
@@ -389,6 +423,15 @@ export default {
margin: 6px 10px 0px 0px;
padding: 8px 22px;
}
.selected {
display: inline-block;
font-size: 12px;
border: 1px solid;
border-radius: 2px;
cursor: pointer;
margin: 6px 10px 0px 0px;
padding: 8px 22px;
}
}
}
}

View File

@@ -94,6 +94,9 @@
<el-form-item label="下载接口" prop='download'>
<el-input v-model="siteInfo.download" :autosize="{ minRows: 2, maxRows: 4}" type="textarea" placeholder="请输入Download接口地址可以空着"/>
</el-form-item>
<el-form-item label="解析接口" prop='jiexiUrl'>
<el-input v-model="siteInfo.jiexiUrl" :autosize="{ minRows: 2, maxRows: 4}" type="textarea" placeholder="请输入解析接口地址默认源自带解析,若要调用应用默认解析接口请输入默认或default"/>
</el-form-item>
<el-form-item label="分组" prop='group'>
<el-select v-model="siteInfo.group" allow-create filterable default-first-option placeholder="请输入分组">
<el-option v-for="item in siteGroup" :key="item" :label="item" :value="item"></el-option>
@@ -133,6 +136,7 @@ export default {
name: '',
api: '',
download: '',
jiexiUrl: '',
group: '',
isActive: true
},
@@ -169,9 +173,9 @@ export default {
},
getFilters () {
const groups = [...new Set(this.sites.map(site => site.group))]
var filters = []
const filters = []
groups.forEach(g => {
var doc = {
const doc = {
text: g,
value: g
}
@@ -271,6 +275,7 @@ export default {
name: '',
api: '',
download: '',
jiexiUrl: '',
group: '',
isActive: true
}
@@ -322,13 +327,14 @@ export default {
if (!this.checkSiteKey()) {
return false
}
var randomstring = require('randomstring')
var doc = {
const randomstring = require('randomstring')
const doc = {
key: this.dialogType === 'edit' ? this.siteInfo.key : this.siteInfo.key ? this.siteInfo.key : randomstring.generate(6),
id: this.dialogType === 'edit' ? this.siteInfo.id : this.sites.length ? this.sites[this.sites.length - 1].id + 1 : 1,
name: this.siteInfo.name,
api: this.siteInfo.api,
download: this.siteInfo.download,
jiexiUrl: this.siteInfo.jiexiUrl,
group: this.siteInfo.group,
isActive: this.siteInfo.isActive
}
@@ -339,6 +345,7 @@ export default {
name: '',
api: '',
download: '',
jiexiUrl: '',
group: ''
}
this.dialogType === 'edit' ? this.$message.success('修改成功!') : this.$message.success('新增源成功!')
@@ -380,7 +387,7 @@ export default {
remote.dialog.showOpenDialog(options).then(result => {
if (!result.canceled) {
result.filePaths.forEach(file => {
var str = fs.readFileSync(file)
const str = fs.readFileSync(file)
const json = JSON.parse(str)
json.forEach(ele => {
if (ele.api && this.sites.filter(x => x.key === ele.key).length === 0 && this.sites.filter(x => x.name === ele.name && x.api === ele.api).length === 0) {
@@ -429,7 +436,7 @@ export default {
sites.add(row)
},
resetId (inArray) {
var id = 1
let id = 1
inArray.forEach(ele => {
ele.id = id
id += 1
@@ -439,7 +446,7 @@ export default {
// 因为el-table的数据是单向绑定,我们先同步el-table里的数据和其绑定的数据
this.syncTableData()
sites.clear().then(res => {
var id = 1
let id = 1
this.sites.forEach(ele => {
ele.id = id
id += 1
@@ -460,7 +467,7 @@ export default {
return false
}
const tbody = document.getElementById('sites-table').querySelector('.el-table__body-wrapper tbody')
var _this = this
const _this = this
Sortable.create(tbody, {
onEnd ({ newIndex, oldIndex }) {
const currRow = _this.sites.splice(oldIndex, 1)[0]

View File

@@ -78,7 +78,7 @@
<el-input-number size="small" v-model="selectedYears.end" :min=0 :max="new Date().getFullYear()" controls-position="right" step-strictly @change="refreshFilteredList"></el-input-number>
</span>
</div>
<el-divider content-position="center">
<el-divider class="listpage-header-divider" content-position="right">
<el-button type="text" size="mini" @click="toggleViewMode">视图切换</el-button>
<el-button type="text" size="mini" @click='() => { showToolbar = !showToolbar; if (!showToolbar) this.refreshFilteredList() }' title="收起工具栏会重置筛选排序">{{ showToolbar ? '隐藏工具栏' : '显示工具栏' }}</el-button>
<el-button type="text" size="mini" @click="backTop">回到顶部</el-button>
@@ -332,6 +332,7 @@ export default {
selectedSiteName: '',
selectedClassName: '',
selectedSearchClassNames: [],
totalpagecount: 0,
pagecount: 0,
recordcount: 0,
list: [],
@@ -403,6 +404,14 @@ export default {
this.SET_SETTING(val)
}
},
DetailCache: {
get () {
return this.$store.getters.getDetailCache
},
set (val) {
this.SET_DetailCache(val)
}
},
filterSettings () {
return this.$store.getters.getSetting.excludeR18Films // 需要监听的数据
},
@@ -468,7 +477,7 @@ export default {
}
},
methods: {
...mapMutations(['SET_VIEW', 'SET_DETAIL', 'SET_VIDEO', 'SET_SHARE', 'SET_SETTING']),
...mapMutations(['SET_VIEW', 'SET_DETAIL', 'SET_VIDEO', 'SET_SHARE', 'SET_SETTING', 'SET_DetailCache']),
backTop () {
const viewMode = this.showFind ? this.setting.searchViewMode : this.setting.view
if (viewMode === 'picture') {
@@ -535,7 +544,7 @@ export default {
return a.localeCompare(b, 'zh')
},
dateFormat (row, column) {
var date = row[column.property]
const date = row[column.property]
if (date === undefined) {
return ''
}
@@ -591,12 +600,14 @@ export default {
if (this.type.name.endsWith('剧')) this.selectedAreas = []
const cacheKey = this.site.key + '@' + this.type.tid
if (FILM_DATA_CACHE[cacheKey]) {
this.totalpagecount = FILM_DATA_CACHE[cacheKey].totalpagecount
this.pagecount = FILM_DATA_CACHE[cacheKey].pagecount
this.recordcount = FILM_DATA_CACHE[cacheKey].recordcount
this.list = FILM_DATA_CACHE[cacheKey].list
this.areas = FILM_DATA_CACHE[cacheKey].areas
} else {
zy.page(this.site.key, this.type.tid).then(res => {
this.totalpagecount = res.pagecount
this.pagecount = res.pagecount
this.recordcount = res.recordcount
this.infiniteId += 1
@@ -609,7 +620,7 @@ export default {
// 屏蔽主分类
const classToHide = ['电影', '电影片', '电视剧', '连续剧', '综艺', '动漫']
zy.class(key).then(res => {
var allClass = [{ name: '最新', tid: 0 }]
const allClass = [{ name: '最新', tid: 0 }]
res.class.forEach(element => {
if (!this.setting.excludeRootClasses || !classToHide.includes(element.name)) {
if (this.setting.excludeR18Films) {
@@ -629,18 +640,28 @@ export default {
})
},
containsR18Keywords (name) {
var containKeyWord = false
const containKeyWord = false
if (!name) {
return containKeyWord
}
return this.r18KeyWords.some(v => name.includes(v))
},
toFlipPagecount () {
// 似乎需要解析的网站的视频排序和其他m3u8采集站的顺序正好相反
if (this.site.jiexiUrl) {
return true
}
return false
},
infiniteHandler ($state) {
const key = this.site.key
const typeTid = this.type.tid
const page = this.pagecount
let page = this.pagecount
if (this.toFlipPagecount()) {
page = this.totalpagecount - this.pagecount + 1
}
this.statusText = ' '
if (key === undefined || page < 1 || typeTid === undefined) {
if (key === undefined || page < 1 || page > this.totalpagecount || typeTid === undefined) {
$state.complete()
this.statusText = '暂无数据'
return false
@@ -654,15 +675,20 @@ export default {
if (res) {
this.pagecount -= 1
const type = Object.prototype.toString.call(res)
if (type === '[object Undefined]') {
$state.complete()
}
if (type === '[object Array]') {
// zy.list 返回的是按时间从旧到新排列, 我门需要翻转为从新到旧
this.list.push(...res.reverse())
}
if (type === '[object Object]') {
this.list.push(res)
// 过滤掉无链接的项
res = res.filter(e => e.dl.dd && (e.dl.dd._t || (Object.prototype.toString.call(e.dl.dd) === '[object Array]' && e.dl.dd.some(i => i._t))))
if (!this.toFlipPagecount()) {
// zy.list 返回的是按时间从旧到新排列, 我门需要翻转为从新到旧
this.list.push(...res.reverse())
} else {
// 如果是需要解析的视频网站zy.list已经是按从新到旧排列
this.list.push(...res)
}
} else if (type === '[object Object]') {
if (res.dl.dd && (res.dl.dd._t || (Object.prototype.toString.call(res.dl.dd) === '[object Array]' && res.dl.dd.some(e => e._t)))) {
this.list.push(res)
}
}
$state.loaded()
// 更新缓存数据
@@ -691,9 +717,6 @@ export default {
} else {
this.video = { key: site.key, info: { id: e.id, name: e.name, index: 0, site: site } }
}
zy.detail(site.key, e.id).then(detailRes => {
this.video.detail = detailRes
})
this.view = 'Play'
},
async starEvent (site, e) {
@@ -701,17 +724,19 @@ export default {
if (db) {
this.$message.info('已存在')
} else {
zy.detail(site.key, e.id).then(detailRes => {
const docs = {
key: site.key,
ids: e.id,
site: site,
name: e.name,
detail: detailRes
}
star.add(docs).then(res => {
this.$message.success('收藏成功')
})
const cacheKey = site.key + '@' + e.id
if (!this.DetailCache[cacheKey]) {
this.DetailCache[cacheKey] = await zy.detail(site.key, e.id)
}
const docs = {
key: site.key,
ids: e.id,
site: site,
name: e.name,
detail: this.DetailCache[cacheKey]
}
star.add(docs).then(res => {
this.$message.success('收藏成功')
})
}
},
@@ -722,36 +747,20 @@ export default {
info: e
}
},
downloadEvent (site, row) {
const key = site.key
const id = row.id
zy.download(key, id).then(res => {
if (res && res.m3u8List) {
const list = res.m3u8List.split('#')
let downloadUrl = ''
for (const i of list) {
const url = encodeURI(i.split('$')[1])
downloadUrl += (url + '\n')
}
clipboard.writeText(downloadUrl)
this.$message.success('『MP4』格式的链接已复制, 快去下载吧!')
} else {
zy.detail(key, id).then(res => {
const list = [...res.m3u8List]
let downloadUrl = ''
for (const i of list) {
const url = encodeURI(i.split('$')[1])
downloadUrl += (url + '\n')
}
clipboard.writeText(downloadUrl)
this.$message.success('『M3U8』格式的链接已复制, 快去下载吧!')
})
}
async downloadEvent (site, row) {
const db = await history.find({ site: site.key, ids: row.id })
let videoFlag
if (db) videoFlag = db.videoFlag
zy.download(site.key, row.id, videoFlag).then(res => {
clipboard.writeText(res.downloadUrls)
this.$message.success(res.info)
}).catch((err) => {
this.$message.error(err.info)
})
},
querySearch (queryString, cb) {
var searchList = this.searchList.slice(0, -1)
var results = queryString ? searchList.filter(this.createFilter(queryString)) : this.searchList
const searchList = this.searchList.slice(0, -1)
const results = queryString ? searchList.filter(this.createFilter(queryString)) : this.searchList
// 调用 callback 返回建议列表的数据
cb(results)
},
@@ -809,20 +818,24 @@ export default {
zy.detail(site.key, element.id).then(detailRes => {
if (id !== this.searchID || !this.searchRunning) return
detailRes.site = site
this.searchContents.push(detailRes)
this.searchContents.sort(function (a, b) {
return a.site.id - b.site.id
})
if (detailRes.dl.dd && (detailRes.dl.dd._t || (Object.prototype.toString.call(detailRes.dl.dd) === '[object Array]' && detailRes.dl.dd.some(i => i._t)))) {
this.searchContents.push(detailRes)
this.searchContents.sort(function (a, b) {
return a.site.id - b.site.id
})
}
}).finally(() => { count++; if (count === res.length) { this.siteSearchCount++; this.statusText = '暂无数据' } })
})
} else if (type === '[object Object]') {
zy.detail(site.key, res.id).then(detailRes => {
if (id !== this.searchID || !this.searchRunning) return
detailRes.site = site
this.searchContents.push(detailRes)
this.searchContents.sort(function (a, b) {
return a.site.id - b.site.id
})
if (detailRes.dl.dd && (detailRes.dl.dd._t || (Object.prototype.toString.call(detailRes.dl.dd) === '[object Array]' && detailRes.dl.dd.some(i => i._t)))) {
this.searchContents.push(detailRes)
this.searchContents.sort(function (a, b) {
return a.site.id - b.site.id
})
}
}).finally(() => { this.siteSearchCount++; this.statusText = '暂无数据' })
} else if (res === undefined) {
this.siteSearchCount++

View File

@@ -35,8 +35,8 @@
width="180"
label="观看至">
<template slot-scope="scope">
<span v-if="scope.row.detail && scope.row.detail.m3u8List && scope.row.detail.m3u8List.length > 1">
{{ scope.row.index + 1 }}({{scope.row.detail.m3u8List.length}})
<span v-if="scope.row.detail && scope.row.detail.fullList[0].list && scope.row.detail.fullList[0].list.length > 1">
{{ scope.row.index + 1 }}({{scope.row.detail.fullList[0].list.length}})
</span>
</template>
</el-table-column>
@@ -45,6 +45,7 @@
label="时间进度">
<template slot-scope="scope">
<span v-if="scope.row.time && scope.row.duration">{{fmtMSS(scope.row.time.toFixed(0))}}/{{fmtMSS(scope.row.duration.toFixed(0))}}</span>
<span v-if="scope.row.onlinePlay">在线解析</span>
</template>
</el-table-column>
<el-table-column
@@ -94,8 +95,9 @@
<span v-if="props.data.time && props.data.duration">
{{fmtMSS(props.data.time.toFixed(0))}}/{{fmtMSS(props.data.duration.toFixed(0))}}
</span>
<span v-if="props.data.detail && props.data.detail.m3u8List !== undefined && props.data.detail.m3u8List.length > 1">
{{ props.data.index + 1 }}({{props.data.detail.m3u8List.length}})
<span v-if="props.data.onlinePlay">在线解析</span>
<span v-if="props.data.detail && props.data.detail.fullList[0].list !== undefined && props.data.detail.fullList[0].list.length > 1">
{{ props.data.index + 1 }}({{props.data.detail.fullList[0].list.length}})
</span>
</div>
</div>
@@ -109,7 +111,6 @@
import { mapMutations } from 'vuex'
import { history, sites, setting } from '../lib/dexie'
import zy from '../lib/site/tools'
import Sortable from 'sortablejs'
import { remote } from 'electron'
import fs from 'fs'
import Waterfall from 'vue-waterfall-plugin'
@@ -230,43 +231,21 @@ export default {
} else {
this.video = { key: e.site, info: { id: e.ids, name: e.name, index: 0 } }
}
zy.detail(e.site, e.ids).then(detailRes => {
this.video.detail = detailRes
})
this.view = 'Play'
},
shareEvent (e) {
this.share = {
show: true,
key: e.site,
info: e
info: e.detail
}
},
downloadEvent (e) {
const key = e.site
const id = e.ids
zy.download(key, id).then(res => {
if (res && res.m3u8List) {
const list = res.m3u8List.split('#')
let downloadUrl = ''
for (const i of list) {
const url = encodeURI(i.split('$')[1])
downloadUrl += (url + '\n')
}
clipboard.writeText(downloadUrl)
this.$message.success('『MP4』格式的链接已复制, 快去下载吧!')
} else {
zy.detail(key, id).then(res => {
const list = [...res.m3u8List]
let downloadUrl = ''
for (const i of list) {
const url = encodeURI(i.split('$')[1])
downloadUrl += (url + '\n')
}
clipboard.writeText(downloadUrl)
this.$message.success('『M3U8』格式的链接已复制, 快去下载吧!')
})
}
zy.download(e.site, e.ids, e.videoFlag).then(res => {
clipboard.writeText(res.downloadUrls)
this.$message.success(res.info)
}).catch((err) => {
this.$message.error(err.info)
})
},
exportHistory () {
@@ -298,7 +277,7 @@ export default {
remote.dialog.showOpenDialog(options).then(result => {
if (!result.canceled) {
result.filePaths.forEach(file => {
var str = fs.readFileSync(file)
const str = fs.readFileSync(file)
const json = JSON.parse(str)
history.bulkAdd(json).then(res => {
this.$message.success('导入成功')
@@ -319,7 +298,7 @@ export default {
})
},
getSiteName (key) {
var site = this.sites.find(e => e.key === key)
const site = this.sites.find(e => e.key === key)
if (site) {
return site.name
}
@@ -333,7 +312,7 @@ export default {
},
updateDatabase () {
history.clear().then(res => {
var id = length
let id = length
this.history.forEach(ele => {
ele.id = id
id -= 1
@@ -341,20 +320,8 @@ export default {
})
})
},
rowDrop () {
const tbody = document.getElementById('history-table').querySelector('.el-table__body-wrapper tbody')
const _this = this
Sortable.create(tbody, {
onEnd ({ newIndex, oldIndex }) {
const currRow = _this.history.splice(oldIndex, 1)[0]
_this.history.splice(newIndex, 0, currRow)
_this.updateDatabase()
}
})
},
updateViewMode () {
if (this.setting.historyViewMode === 'table') {
setTimeout(() => { this.rowDrop() }, 100)
this.showShiftPrompt()
} else {
setTimeout(() => { if (this.$refs.historyWaterfall) this.$refs.historyWaterfall.refresh() }, 700)
@@ -377,7 +344,6 @@ export default {
}
},
mounted () {
if (this.setting.historyViewMode === 'table') setTimeout(() => { this.rowDrop() }, 100)
addEventListener('keydown', code => { if (code.keyCode === 16) this.shiftDown = true })
addEventListener('keyup', code => { if (code.keyCode === 16) this.shiftDown = false })
addEventListener('resize', () => {

View File

@@ -168,9 +168,9 @@ export default {
},
getFilters () {
const groups = [...new Set(this.channelList.map(iptv => iptv.group))]
var filters = []
const filters = []
groups.forEach(g => {
var doc = {
const doc = {
text: g,
value: g
}
@@ -257,7 +257,7 @@ export default {
},
mergeChannel () {
if (this.inputContent && this.multipleSelection.length) {
var channels = []
let channels = []
const id = this.multipleSelection[0].id
this.multipleSelection.forEach(ele => {
channels = channels.concat(ele.channels)
@@ -320,7 +320,7 @@ export default {
remote.dialog.showSaveDialog(options).then(result => {
if (!result.canceled) {
if (result.filePath.endsWith('m3u')) {
var writer = require('m3u').extendedWriter()
const writer = require('m3u').extendedWriter()
this.iptvList.forEach(e => {
writer.file(e.url, -1, e.name)
})
@@ -360,11 +360,12 @@ export default {
const parser = require('iptv-playlist-parser')
const playlist = fs.readFileSync(file, { encoding: 'utf-8' })
const result = parser.parse(playlist)
const supportFormats = /\.(m3u8|flv)$/
result.items.forEach(ele => {
const urls = ele.url.split('#').filter(e => e.startsWith('http')) // 网址带#时自动分割
urls.forEach(url => {
if (ele.name && url && new URL.URL(url).pathname.endsWith('.m3u8')) { // 网址可能带参数
var doc = {
if (ele.name && url && (supportFormats.test(url) || supportFormats.test(new URL.URL(url).pathname))) { // 网址可能带参数
const doc = {
id: id,
name: ele.name,
url: url,
@@ -438,12 +439,12 @@ export default {
res = res.filter(o => !this.iptvList.find(e => o.url === e.url))
const resClone = JSON.parse(JSON.stringify(res))
const uniqueChannelName = {}
for (var i = 0; i < resClone.length; i++) {
var channelName = resClone[i].name.trim().replace(/[- ]?(1080p|蓝光|超清|高清|标清|hd|cq|4k)(\d{1,2})?$/i, '')
for (let i = 0; i < resClone.length; i++) {
let channelName = resClone[i].name.trim().replace(/[- ]?(1080p|蓝光|超清|高清|标清|hd|cq|4k)(\d{1,2})?$/i, '')
if (channelName.match(/cctv/i)) channelName = channelName.replace('-', '')
if (Object.keys(uniqueChannelName).some(name => channelName.match(new RegExp(`${name}(1080p|4k|(?!\\d))`, 'i')))) continue // 避免重复
const matchRule = new RegExp(`${channelName}(1080p|4k|(?!\\d))`, 'i')
for (var j = i; j < resClone.length; j++) {
for (let j = i; j < resClone.length; j++) {
if (resClone[j].name.match(/cctv/i)) {
resClone[j].name = resClone[j].name.replace('-', '')
}
@@ -513,7 +514,7 @@ export default {
})
},
resetId (channelList) {
var id = 1
let id = 1
channelList.forEach(ele => {
ele.id = id
id += 1
@@ -575,7 +576,7 @@ export default {
})
},
async checkChannelsBySite (channels) {
var siteList = {}
const siteList = {}
channels.forEach(channel => {
const site = channel.url.split('/')[2]
if (siteList[site]) {
@@ -592,44 +593,49 @@ export default {
await this.checkSingleChannel(c)
}
},
async checkSingleChannel (channel) {
if (this.setting.allowPassWhenIptvCheck && !channel.isActive) {
async checkSingleChannel (channel, force = false) {
if (this.stopFlag) {
this.checkProgress += 1
return
}
channel.status = ' '
const ele = this.channelList.find(e => e.id === channel.channelID)
if (this.stopFlag) {
this.checkProgress += 1
return channel.status
}
const flag = await zy.checkChannel(channel.url)
this.checkProgress += 1
ele.hasCheckedNum++
if (flag) {
channel.status = '可用'
if (!force && this.setting.allowPassWhenIptvCheck && (!channel.isActive || !ele.isActive)) {
if (!ele.isActive) {
ele.status = '跳过'
} else if (!channel.isActive) {
channel.status = '跳过'
}
} else {
channel.status = '失效'
channel.isActive = false
if (this.setting.autocleanWhenIptvCheck) {
ele.channels.splice(ele.channels.findIndex(e => e.id === channel.id), 1)
ele.hasCheckedNum--
channel.status = ' '
const flag = await zy.checkChannel(channel.url)
if (flag) {
channel.status = '可用'
} else {
channel.status = '失效'
channel.isActive = false
if (this.setting.autocleanWhenIptvCheck) {
ele.channels.splice(ele.channels.findIndex(e => e.id === channel.id), 1)
ele.hasCheckedNum--
}
}
}
this.checkProgress += 1
ele.hasCheckedNum++
if (ele.hasCheckedNum === ele.channels.length) {
ele.status = ele.channels.some(channel => channel.status === '可用') ? '可用' : '失效'
if (ele.status === '失效') ele.isActive = false
if (ele.status === ' ') {
ele.status = ele.channels.some(channel => channel.status === '可用') ? '可用' : '失效'
if (ele.status === '失效') ele.isActive = false
}
channelList.remove(channel.channelID)
if (ele.channels.length === 1) ele.hasChildren = false
if (ele.channels.length) channelList.add(ele)
}
return channel.status
},
async checkChannel (row) {
if (row.channels) {
row.status = ' '
row.hasCheckedNum = 0
row.channels.forEach(e => this.checkSingleChannel(e))
row.channels.forEach(e => this.checkSingleChannel(e, true))
} else {
this.checkSingleChannel(row)
}

View File

@@ -3,12 +3,23 @@
<div class="box">
<div class="title">
<span v-if="this.right.list.length > 1"> {{(video.info.index + 1)}} </span>{{name}}
<span class="right" @click="() => { onlineUrl = ''; videoStop(); }">
<svg role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" aria-labelledby="closeIconTitle">
<title id="closeIconTitle">关闭</title>
<path d="M6.34314575 6.34314575L17.6568542 17.6568542M6.34314575 17.6568542L17.6568542 6.34314575"></path>
</svg>
</span>
</div>
<div class="player">
<div class="player" v-show="!onlineUrl">
<div id="xgplayer"></div>
</div>
<div class="iframePlayer" v-if="onlineUrl" style='width:100%;height:100%;'>
<iframe v-bind:src="onlineUrl" width="100%" height="100%"
frameborder="0" scrolling="no" allow='autoplay;fullscreen'>
</iframe>
</div>
<div class="more" v-if="!video.iptv" :key="Boolean(video.iptv)">
<span class="zy-svg" @click="otherEvent" v-show="name !== ''">
<span class="zy-svg" @click="otherEvent" v-show="name !== ''" :class="right.type === 'other' ? 'active' : ''">
<svg role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" aria-labelledby="coloursIconTitle">
<title id="coloursIconTitle">换源</title>
<circle cx="12" cy="9" r="5"></circle>
@@ -84,7 +95,7 @@
<rect x="17" y="6" width="1" height="1"></rect>
</svg>
</span>
<span class="zy-svg" @click="showShortcutEvent" v-show="right.list.length > 0">
<span class="zy-svg" @click="showShortcutEvent" :class="right.type === 'shortcut' ? 'active' : ''" v-show="right.list.length > 0">
<svg role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" aria-labelledby="sendIconTitle">
<title id="sendIconTitle">快捷键指南</title>
<polygon points="21.368 12.001 3 21.609 3 14 11 12 3 9.794 3 2.394"></polygon>
@@ -98,7 +109,7 @@
<circle cx="12" cy="12" r="10"></circle>
</svg>
</span>
<span class="timespanSwitch" v-if="right.list.length > 1" title="跳过片头片尾,建议优先通过快捷键设置,更便捷更精准">
<span class="timespanSwitch" v-if="right.list.length > 1 && !onlineUrl" title="跳过片头片尾,建议优先通过快捷键设置,更便捷更精准">
<el-switch v-model="state.showTimespanSetting" active-text="手动跳略时长"></el-switch>
</span>
<span class="timespan" v-if="state.showTimespanSetting">
@@ -116,11 +127,14 @@
<span></span>
<input type="button" value="重置" @click="() => { startPosition.min = startPosition.sec = endPosition.min = endPosition.sec = '00'; this.clearPosition() }">
</span>
<span class="last-tip" v-if="!video.key && right.history.length > 0" @click="historyItemEvent(right.history[0])">
上次播放到:{{right.history[0].site}}{{right.history[0].name}} {{right.history[0].index+1}} {{fmtMSS(right.history[0].time.toFixed(0))}}/{{fmtMSS(right.history[0].duration.toFixed(0))}}</span>
<span class="last-tip" v-if="!video.key && right.history.length > 0 && right.history[0]" @click="historyItemEvent(right.history[0])">
<span>上次播放到:{{right.history[0].site}}{{right.history[0].name}} {{right.history[0].index+1}} </span>
<span v-if="right.history[0].time && right.history[0].duration">{{fmtMSS(right.history[0].time.toFixed(0))}}/{{fmtMSS(right.history[0].duration.toFixed(0))}}</span>
<span v-if="right.history[0].onlinePlay">在线解析</span>
</span>
</div>
<div class="more" v-if="video.iptv" :key="Boolean(video.iptv)">
<span class="zy-svg" @click="state.showChannelList = !state.showChannelList">
<span class="zy-svg" @click="state.showChannelList = !state.showChannelList" :class="state.showChannelList ? 'active' : ''">
<svg role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" aria-labelledby="dashboardIconTitle">
<title id="dashboardIconTitle">频道列表</title>
<rect width="20" height="20" x="2" y="2"></rect>
@@ -130,7 +144,7 @@
<line x1="7" y1="17" x2="7" y2="17"></line>
</svg>
</span>
<span class="zy-svg" @click="otherEvent">
<span class="zy-svg" @click="otherEvent" :class="right.type === 'sources' ? 'active' : ''">
<svg role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" aria-labelledby="coloursIconTitle">
<title id="coloursIconTitle">换源</title>
<circle cx="12" cy="9" r="5"></circle>
@@ -151,7 +165,7 @@
<polyline stroke-linejoin="round" points="8 4 12 7.917 16 4"></polyline>
</svg>
</span>
<span class="zy-svg" @click="showShortcutEvent">
<span class="zy-svg" @click="showShortcutEvent" :class="right.type === 'shortcut' ? 'active' : ''">
<svg role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" aria-labelledby="sendIconTitle">
<title id="sendIconTitle">快捷键指南</title>
<polygon points="21.368 12.001 3 21.609 3 14 11 12 3 9.794 3 2.394"></polygon>
@@ -175,24 +189,24 @@
</span>
</div>
<div class="list-body zy-scroll" :style="{overflowY:scroll? 'auto' : 'hidden',paddingRight: scroll ? '0': '5px' }" @mouseenter="scroll = true" @mouseleave="scroll = false">
<ul v-if="right.type === 'list'" class="list-item" v-on-clickaway="closeListEvent">
<li v-if="right.list.length > 0" @click="exportM3u8">导出</li>
<ul v-if="right.type === 'list'" class="list-item" v-clickoutside="closeListEvent">
<li v-if="exportablePlaylist" @click="exportM3u8">导出</li>
<li v-if="right.list.length === 0">无数据</li>
<li @click="listItemEvent(j)" :class="video.info.index === j ? 'active' : ''" v-for="(i, j) in right.list" :key="j">{{i | ftName(j)}}</li>
</ul>
<ul v-if="right.type === 'history'" class="list-history" v-on-clickaway="closeListEvent">
<ul v-if="right.type === 'history'" class="list-history" v-clickoutside="closeListEvent">
<li v-if="right.history.length > 0" @click="clearAllHistory">清空</li>
<li v-if="right.history.length === 0">无数据</li>
<li @click="historyItemEvent(m)" :class="video.info.id === m.ids ? 'active' : ''" v-for="(m, n) in right.history" :key="n"><span class="title" :title="'【' + m.site + '】' + m.name + ' 第' + (m.index+1) + '集'">【{{m.site}}】{{m.name}} 第{{m.index+1}}集</span><span @click.stop="removeHistoryItem(m)" class="detail-delete">删除</span></li>
</ul>
<ul v-if="right.type === 'shortcut'" class="list-shortcut" v-on-clickaway="closeListEvent">
<ul v-if="right.type === 'shortcut'" class="list-shortcut" v-clickoutside="closeListEvent">
<li v-for="(m, n) in right.shortcut" :key="n"><span class="title">{{m.desc}} -- [ {{m.key}} ]</span></li>
</ul>
<ul v-if="right.type === 'other'" class="list-other" v-on-clickaway="closeListEvent">
<ul v-if="right.type === 'other'" class="list-other" v-clickoutside="closeListEvent">
<li v-if="right.other.length === 0">无数据</li>
<li @click="otherItemEvent(m)" v-for="(m, n) in right.other" :key="n"><span class="title">{{m.name}} - [{{m.site.name}}]</span></li>
</ul>
<ul v-if="right.type === 'sources'" class="list-channels" v-on-clickaway="closeListEvent">
<ul v-if="right.type === 'sources'" class="list-channels" v-clickoutside="closeListEvent">
<li v-if="right.sources.length === 0">当前频道已关闭</li>
<li v-for="(channel, index) in right.sources" :key="index">
<span @click="playChannel(channel)" class="title">{{ channel.id === video.iptv.id ? channel.name + '[当前]' : channel.name }}</span>
@@ -203,7 +217,7 @@
</div>
</transition>
<transition name="slideX">
<div v-if="state.showChannelList" class="list" v-on-clickaway="closeListEvent">
<div v-if="state.showChannelList" class="list" v-clickoutside="closeListEvent">
<div class="list-top">
<span class="list-top-title">频道列表</span>
<span class="list-top-close zy-svg" @click="state.showChannelList = false">
@@ -239,14 +253,15 @@ import { star, history, setting, shortcut, mini, channelList, sites } from '../l
import zy from '../lib/site/tools'
import Player from 'xgplayer'
import HlsJsPlayer from 'xgplayer-hls.js'
import FlvJsPlayer from 'xgplayer-flv.js'
import mt from 'mousetrap'
import { directive as onClickaway } from 'vue-clickaway'
import Clickoutside from 'element-ui/src/utils/clickoutside'
import { exec, execFile } from 'child_process'
const { remote, clipboard } = require('electron')
const win = remote.getCurrentWindow()
const PinyinMatch = require('pinyin-match')
const URL = require('url')
const VIDEO_DETAIL_CACHE = {}
const addPlayerBtn = function (event, svg, attrs) {
@@ -327,7 +342,7 @@ export default {
videoTitle: true,
airplay: true,
closeVideoTouch: true,
ignores: ['cssFullscreen', 'replay'],
ignores: ['cssFullscreen', 'replay', 'error'], // 为了切换播放器类型时避免显示错误刷新,暂时忽略错误
preloadTime: 300
},
state: {
@@ -341,7 +356,6 @@ export default {
timer: null,
scroll: false,
isStar: false,
isTop: false,
miniMode: false,
mainWindowBounds: {},
searchTxt: '',
@@ -355,7 +369,11 @@ export default {
startPosition: { min: '00', sec: '00' }, // 对应调略输入框
endPosition: { min: '00', sec: '00' },
skipendStatus: false, // 是否跳过了片尾
currentShortcutList: []
currentShortcutList: [],
onlineUrl: '',
playerType: 'hls',
exportablePlaylist: false,
changingIPTV: false
}
},
filters: {
@@ -369,7 +387,7 @@ export default {
}
},
directives: {
onClickaway: onClickaway
Clickoutside
},
computed: {
view: {
@@ -419,6 +437,14 @@ export default {
set (val) {
this.SET_SETTING(val)
}
},
DetailCache: {
get () {
return this.$store.getters.getDetailCache
},
set (val) {
this.SET_DetailCache(val)
}
}
},
watch: {
@@ -434,6 +460,7 @@ export default {
},
video: {
handler () {
if (this.changingIPTV) return
this.getUrls()
},
deep: true
@@ -472,7 +499,7 @@ export default {
}
},
methods: {
...mapMutations(['SET_VIEW', 'SET_DETAIL', 'SET_VIDEO', 'SET_SHARE', 'SET_APPSTATE']),
...mapMutations(['SET_VIEW', 'SET_DETAIL', 'SET_VIDEO', 'SET_SHARE', 'SET_APPSTATE', 'SET_DetailCache']),
fmtMSS (s) {
return (s - (s %= 60)) / 60 + (s > 9 ? ':' : ':0') + s
},
@@ -502,6 +529,7 @@ export default {
return false
}
this.name = ''
this.onlineUrl = ''
if (this.timer !== null) {
clearInterval(this.timer)
this.timer = null
@@ -518,14 +546,16 @@ export default {
const index = this.video.info.index || 0
const db = await history.find({ site: this.video.key, ids: this.video.info.id })
const key = this.video.key + '@' + this.video.info.id
var time = this.video.info.time
let time = this.video.info.time
this.xg.removeAllProgressDot()
this.startPosition = this.endPosition = { min: '00', sec: '00' }
this.startPosition = { min: '00', sec: '00' }
this.endPosition = { min: '00', sec: '00' }
if (db) {
if (!time && db.index === index) { // 如果video.info.time没有设定的话从历史中读取时间进度
time = db.time
}
if (!VIDEO_DETAIL_CACHE[key]) VIDEO_DETAIL_CACHE[key] = {}
if (!this.video.info.videoFlag) this.video.info.videoFlag = db.videoFlag
if (db.startPosition) { // 数据库保存的时长通过快捷键设置时可能为小数, this.startPosition为object对应输入框分秒转化到数据库后肯定为整数
VIDEO_DETAIL_CACHE[key].startPosition = db.startPosition
this.startPosition = { min: '' + parseInt(db.startPosition / 60), sec: '' + parseInt(db.startPosition % 60) }
@@ -560,119 +590,162 @@ export default {
channelList.add(ele)
this.right.sources = ele.channels.filter(e => e.isActive)
}
this.changingIPTV = true // 避免二次执行playChannel
this.video.iptv = channel
this.name = channel.name
const supportFormats = /\.(m3u8|flv)$/
const extRE = channel.url.match(supportFormats) || new URL.URL(channel.url).pathname.match(supportFormats)
this.getPlayer(extRE[1])
if (extRE[1] === 'flv') this.xg.config.isLive = true
this.xg.src = channel.url
this.xg.play()
document.querySelector('xg-btn-showhistory').style.display = 'none'
document.querySelector('.xgplayer-playbackrate').style.display = 'none'
this.changingIPTV = false
if (document.querySelector('xg-btn-showhistory')) document.querySelector('xg-btn-showhistory').style.display = 'none'
if (document.querySelector('.xgplayer-playbackrate')) document.querySelector('.xgplayer-playbackrate').style.display = 'none'
},
getPlayer (playerType, force = false) {
if (!force && this.playerType === playerType) return
if (this.playerType !== 'flv') {
this.xg.src = '' // https://developers.google.com/web/updates/2017/06/play-request-was-interrupted#danger-zone
this.config.url = ''
}
try {
this.xg.destroy()
} catch (err) { }
this.xg = null
switch (playerType) {
case 'mp4':
this.xg = new Player(this.config)
break
case 'flv':
this.xg = new FlvJsPlayer(this.config)
break
default:
this.xg = new HlsJsPlayer(this.config)
}
this.playerInstall()
this.bindEvent()
this.playerType = playerType
},
playVideo (index = 0, time = 0) {
this.isLive = false
document.querySelector('xg-btn-showhistory').style.display = 'block'
document.querySelector('.xgplayer-playbackrate').style.display = 'inline-block'
this.fetchM3u8List().then(m3u8Arr => {
const url = m3u8Arr[index]
if (!m3u8Arr[index].endsWith('.m3u8')) {
const onlineUrl = 'https://www.1717yun.com/jiexi/?url=' + url
const open = require('open')
open(onlineUrl)
} else {
this.xg.src = m3u8Arr[index]
const key = this.video.key + '@' + this.video.info.id
const startTime = VIDEO_DETAIL_CACHE[key].startPosition || 0
this.xg.play()
this.xg.once('playing', () => {
this.xg.currentTime = time > startTime ? time : startTime
if (VIDEO_DETAIL_CACHE[key].startPosition) this.xg.addProgressDot(VIDEO_DETAIL_CACHE[key].startPosition, '片头')
if (VIDEO_DETAIL_CACHE[key].endPosition) this.xg.addProgressDot(this.xg.duration - VIDEO_DETAIL_CACHE[key].endPosition, '片尾')
})
this.videoPlaying()
this.skipendStatus = false
this.xg.once('ended', () => {
if (m3u8Arr.length > 1 && (m3u8Arr.length - 1 > index)) {
this.video.info.time = 0
this.video.info.index++
}
this.xg.off('ended') // 明明是once为何会触发多次得注销掉以真正只执行一次
})
this.exportablePlaylist = false
this.fetchPlaylist().then(async (fullList) => {
let playlist = fullList[0].list // ZY支持的已移到首位
// 如果设定了特定的video flag, 获取该flag下的视频列表
const videoFlag = this.video.info.videoFlag
if (videoFlag) {
playlist = fullList.find(x => x.flag === videoFlag).list
}
this.right.list = playlist
const url = playlist[index].includes('$') ? playlist[index].split('$')[1] : playlist[index]
if (playlist.every(e => e.includes('$') ? e.split('$')[1].endsWith('.m3u8') : e.endsWith('.m3u8'))) this.exportablePlaylist = true
if (!url.endsWith('.m3u8') && !url.endsWith('.mp4')) {
const currentSite = await sites.find({ key: this.video.key })
this.$message.info('即将调用解析接口播放,请等待...')
if (currentSite.jiexiUrl) {
this.onlineUrl = /^\s*(default|默认)\s*$/i.test(currentSite.jiexiUrl) ? this.setting.defaultParseURL + url : currentSite.jiexiUrl + url
} else {
this.onlineUrl = url
}
this.videoPlaying('online')
return
} else {
const ext = url.match(/\.\w+?$/)[0].slice(1)
this.getPlayer(ext)
}
this.xg.src = url
const key = this.video.key + '@' + this.video.info.id
const startTime = VIDEO_DETAIL_CACHE[key].startPosition || 0
this.xg.play()
if (document.querySelector('xg-btn-showhistory')) document.querySelector('xg-btn-showhistory').style.display = 'block'
if (document.querySelector('.xgplayer-playbackrate')) document.querySelector('.xgplayer-playbackrate').style.display = 'inline-block'
this.xg.once('playing', () => {
this.xg.currentTime = time > startTime ? time : startTime
if (VIDEO_DETAIL_CACHE[key].startPosition) this.xg.addProgressDot(VIDEO_DETAIL_CACHE[key].startPosition, '片头')
if (VIDEO_DETAIL_CACHE[key].endPosition) this.xg.addProgressDot(this.xg.duration - VIDEO_DETAIL_CACHE[key].endPosition, '片尾')
})
this.videoPlaying()
this.skipendStatus = false
this.xg.once('ended', () => {
if (playlist.length > 1 && (playlist.length - 1 > index)) {
this.video.info.time = 0
this.video.info.index++
}
this.xg.off('ended') // 明明是once为何会触发多次得注销掉以真正只执行一次
})
})
},
fetchM3u8List () {
fetchPlaylist () {
return new Promise((resolve) => {
const cacheKey = this.video.key + '@' + this.video.info.id
if (VIDEO_DETAIL_CACHE[cacheKey] && VIDEO_DETAIL_CACHE[cacheKey].list && VIDEO_DETAIL_CACHE[cacheKey].list.length) {
this.name = VIDEO_DETAIL_CACHE[cacheKey].name
resolve(VIDEO_DETAIL_CACHE[cacheKey].list)
}
zy.detail(this.video.key, this.video.info.id).then(res => {
let res
if (!this.DetailCache[cacheKey]) {
zy.detail(this.video.key, this.video.info.id).then(res => {
this.DetailCache[cacheKey] = res
res = this.DetailCache[cacheKey]
this.name = res.name
VIDEO_DETAIL_CACHE[cacheKey] = Object.assign(VIDEO_DETAIL_CACHE[cacheKey] || { }, {
list: res.fullList,
name: res.name
})
resolve(res.fullList)
})
} else {
res = this.DetailCache[cacheKey]
this.name = res.name
const m3u8Txt = res.m3u8List
this.right.list = m3u8Txt
const m3u8Arr = []
for (const i of m3u8Txt) {
const j = i.split('$')
if (j.length > 1) {
for (let m = 0; m < j.length; m++) {
if (j[m].startsWith('http')) {
m3u8Arr.push(j[m])
break
}
}
} else {
m3u8Arr.push(j[0])
}
}
VIDEO_DETAIL_CACHE[cacheKey] = Object.assign(VIDEO_DETAIL_CACHE[cacheKey] || {}, {
list: m3u8Arr,
VIDEO_DETAIL_CACHE[cacheKey] = Object.assign(VIDEO_DETAIL_CACHE[cacheKey] || { }, {
list: res.fullList,
name: res.name
})
resolve(m3u8Arr)
})
resolve(res.fullList)
}
})
},
async videoPlaying () {
this.changeVideo()
async videoPlaying (isOnline) {
const db = await history.find({ site: this.video.key, ids: this.video.info.id })
const videoFlag = this.video.info.videoFlag || ''
let time = this.xg.currentTime || 0
let duration = this.xg.duration || 0
let startPosition = 0
let endPosition = 0
if (db) {
const doc = {
site: db.site,
ids: db.ids,
name: db.name,
type: db.type,
year: db.year,
index: this.video.info.index,
time: db.time,
detail: this.video.detail
}
history.update(db.id, doc)
} else {
const doc = {
site: this.video.key,
ids: this.video.info.id,
name: this.video.info.name,
type: this.video.info.type,
year: this.video.info.year,
index: this.video.info.index,
time: '',
detail: this.video.detail
}
history.add(doc)
time = time || db.time
duration = duration || db.duration
startPosition = db.startPosition
endPosition = db.endPosition
await history.remove(db.id)
}
if (isOnline) {
time = duration = 0
}
const doc = {
site: this.video.key,
ids: this.video.info.id,
name: this.video.info.name,
type: this.video.info.type,
year: this.video.info.year,
index: this.video.info.index,
time: time,
duration: duration,
startPosition: startPosition,
endPosition: endPosition,
detail: this.DetailCache[this.video.key + '@' + this.video.info.id],
onlinePlay: isOnline,
videoFlag: videoFlag
}
await history.add(doc)
this.updateStar()
this.timerEvent()
},
changeVideo () {
win.setProgressBar(-1)
this.checkStar()
this.checkTop()
if (!isOnline) this.timerEvent()
},
async setProgressDotEvent (position, timespan, text) { // 根据跳略时长在进度条上添加标记, position为位置, timespan为时长text为标记文本(title)
const key = this.video.key + '@' + this.video.info.id
const db = await history.find({ site: this.video.key, ids: this.video.info.id })
if (db && this.xg && VIDEO_DETAIL_CACHE[key].list.length > 1) {
if (db && this.xg && this.right.list.length > 1) {
this[position] = { min: '' + parseInt(timespan / 60), sec: '' + parseInt(timespan % 60) }
const positionTime = position === 'endPosition' ? this.xg.duration - timespan : timespan
if (db[position]) this.xg.removeProgressDot(position === 'endPosition' ? this.xg.duration - db[position] : db[position])
@@ -702,10 +775,6 @@ export default {
},
timerEvent () {
this.timer = setInterval(async () => {
const endTime = this.xg.duration
const currentTime = this.xg.currentTime
const progress = parseFloat((currentTime / endTime).toFixed(2))
win.setProgressBar(progress)
const db = await history.find({ site: this.video.key, ids: this.video.info.id })
if (db) {
const doc = { ...db }
@@ -718,13 +787,10 @@ export default {
},
prevEvent () {
if (this.video.iptv) {
var index = this.channelList.findIndex(obj => obj.id === this.video.iptv.channelID)
if (index >= 1) {
var channel = this.channelList[index - 1]
this.playChannel(channel)
} else {
this.$message.warning('这已经是第一个频道了。')
}
let index = this.channelList.findIndex(obj => obj.id === this.video.iptv.channelID)
index = index === 0 ? this.channelList.length - 1 : index - 1
const channel = this.channelList[index]
this.playChannel(channel)
} else {
if (this.video.info.index >= 1) {
this.video.info.index--
@@ -736,13 +802,10 @@ export default {
},
nextEvent () {
if (this.video.iptv) {
var index = this.channelList.findIndex(obj => obj.id === this.video.iptv.channelID)
if (index < (this.channelList.length - 1)) {
var channel = this.channelList[index + 1]
this.playChannel(channel)
} else {
this.$message.warning('这已经是最后一个频道了。')
}
let index = this.channelList.findIndex(obj => obj.id === this.video.iptv.channelID)
index = index === this.channelList.length - 1 ? 0 : index + 1
const channel = this.channelList[index]
this.playChannel(channel)
} else {
if (this.video.info.index < (this.right.list.length - 1)) {
this.video.info.index++
@@ -781,13 +844,15 @@ export default {
const info = this.video.info
const db = await star.find({ key: this.video.key, ids: info.id })
if (db) {
this.isStar = true
db.index = info.index
star.update(db.id, db)
} else {
this.isStar = false
}
},
async starEvent () {
const info = this.video.info
const db = await star.find({ key: this.video.key, ids: info.id })
const db = await star.find({ key: this.video.key, ids: this.video.info.id })
if (db) {
star.remove(db.id).then(res => {
if (res) {
@@ -798,18 +863,16 @@ export default {
}
})
} else {
zy.detail(this.video.key, info.id).then(detailRes => {
const docs = {
key: this.video.key,
ids: info.id,
name: info.name,
detail: detailRes,
index: info.index
}
star.add(docs).then(res => {
this.$message.success('收藏成功')
this.isStar = true
})
const docs = {
key: this.video.key,
ids: this.video.info.id,
name: this.video.info.name,
detail: this.DetailCache[this.video.key + '@' + this.video.info.id],
index: this.video.info.index
}
star.add(docs).then(res => {
this.$message.success('收藏成功')
this.isStar = true
})
}
},
@@ -852,14 +915,16 @@ export default {
this.share = {
show: true,
key: this.video.key,
info: this.video.info
info: this.DetailCache[this.video.key + '@' + this.video.info.id],
index: this.video.info.index
}
},
issueEvent () {
async issueEvent () {
const currentSite = await sites.find({ key: this.video.key })
const info = {
video: this.video,
list: this.right.list,
m3u8List: VIDEO_DETAIL_CACHE[this.video.key + '@' + this.video.info.id] || [],
video: Object.assign({ site: currentSite, detail: this.DetailCache[this.video.key + '@' + this.video.info.id] }, this.video.info),
playlist: this.right.list.map(e => e.split('$')[1]),
playerType: this.onlineUrl ? '在线解析' : this.playerType,
playerError: this.xg.error || '',
playerState: this.xg.readyState || '',
networkState: this.xg.networkState || ''
@@ -869,8 +934,8 @@ export default {
},
playWithExternalPalyerEvent () {
const fs = require('fs')
const externalPlayer = this.setting.externalPlayer
if (this.video.iptv) {
var externalPlayer = this.setting.externalPlayer
if (!externalPlayer) {
this.$message.error('请设置第三方播放器路径')
return
@@ -880,35 +945,40 @@ export default {
} else {
exec(externalPlayer, [this.video.iptv.url])
}
return
}
this.fetchM3u8List().then(m3u8Arr => {
var externalPlayer = this.setting.externalPlayer
} else {
const playlistUrls = this.right.list.map(e => e.split('$')[1])
if (!externalPlayer) {
this.$message.error('请设置第三方播放器路径')
// 在线播放该视频
var link = 'https://www.m3u8play.com/?play=' + m3u8Arr[this.video.info.index]
const open = require('open')
open(link)
if (playlistUrls[this.video.info.index].endsWith('.m3u8')) {
const link = 'http://hunlongyu.gitee.io/zy-player-web?url=' + playlistUrls[this.video.info.index] + '&name=' + this.video.info.name
const open = require('open')
open(link)
}
} else {
var m3uFile = this.generateM3uFile(this.video.info.name, m3u8Arr, this.video.info.index)
if (fs.existsSync(externalPlayer)) {
execFile(externalPlayer, [m3uFile])
let playlist
if (playlistUrls.every(e => e.endsWith('.m3u8'))) {
playlist = this.generateM3uFile(this.video.info.name, playlistUrls, this.video.info.index)
} else {
exec(externalPlayer, [m3uFile])
playlist = playlistUrls[this.video.info.index]
}
if (fs.existsSync(externalPlayer)) {
execFile(externalPlayer, [playlist])
} else {
exec(externalPlayer, [playlist])
}
}
})
}
},
generateM3uFile (fileName, m3u8Arr, startIndex) {
const path = require('path')
const os = require('os')
const fs = require('fs')
var filePath = path.join(os.tmpdir(), fileName + '.m3u')
const filePath = path.join(os.tmpdir(), fileName + '.m3u')
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath)
}
var str = '#EXTM3U' + os.EOL
let str = '#EXTM3U' + os.EOL
for (let ind = startIndex; ind < m3u8Arr.length; ind++) {
str += `#EXTINF: -1, 第${ind + 1}` + os.EOL
str += m3u8Arr[ind] + os.EOL
@@ -917,21 +987,18 @@ export default {
fs.writeFileSync(filePath, str)
return filePath
},
async checkStar () {
const db = await star.find({ key: this.video.key, ids: this.video.info.id })
if (db) {
this.isStar = true
} else {
this.isStar = false
}
},
checkTop () {
this.isTop = this.appState.windowIsOnTop
},
closeListEvent () {
this.right.show = false
this.right.type = ''
this.state.showChannelList = false
const lastRightType = this.right.type
const lastChannelListState = this.state.showChannelList
setTimeout(() => {
if (lastRightType === this.right.type) {
this.right.show = false
this.right.type = ''
}
if (lastChannelListState === this.state.showChannelList) {
this.state.showChannelList = false
}
}, 50)
},
exportM3u8 () {
const m3u8Arr = []
@@ -955,15 +1022,15 @@ export default {
link: link
})
}
let m3u8Content = '#EXTM3U'
let m3u8Content = '#EXTM3U\n'
for (const item of m3u8Arr) {
m3u8Content += `#EXTINF:-1, ${item.name}\n${item.link}`
m3u8Content += `#EXTINF:-1, ${item.name}\n${item.link}\n`
}
const blob = new Blob([m3u8Content], { type: 'application/vnd.apple.mpegurl' })
const downloadElement = document.createElement('a')
const href = window.URL.createObjectURL(blob)
downloadElement.href = href
downloadElement.download = `${this.name}.m3u8`
downloadElement.download = `${this.name}.m3u`
document.body.appendChild(downloadElement)
downloadElement.click()
document.body.removeChild(downloadElement)
@@ -976,7 +1043,7 @@ export default {
},
listItemEvent (n) {
if (this.video.iptv) {
var channel = this.channelList[n]
const channel = this.channelList[n]
// 是直播源,直接播放
this.playChannel(channel)
} else {
@@ -1040,7 +1107,6 @@ export default {
this.getOtherSites()
this.right.currentTime = this.xg.currentTime
} else {
this.state.showChannelList = false
this.right.type = 'sources'
}
this.right.show = true
@@ -1084,8 +1150,6 @@ export default {
if (this.xg) {
if (this.xg.paused) {
this.xg.play()
// 继续播放时,隐藏进度条
win.setProgressBar(-1)
} else {
this.xg.pause()
}
@@ -1341,7 +1405,7 @@ export default {
this.channelTree = []
const groups = [...new Set(this.channelList.map(iptv => iptv.group))]
groups.forEach(g => {
var doc = {
const doc = {
label: g,
children: this.channelList.filter(x => x.group === g).map(i => { return { label: i.name, channel: i } })
}
@@ -1350,6 +1414,30 @@ export default {
})
},
bindEvent () {
// 直播卡顿时换源换台
let stallIptvTimeout
let stallCount = 0
this.xg.on('waiting', () => {
if (this.isLive && this.setting.autoChangeSourceWhenIptvStalling) {
stallIptvTimeout = setTimeout(() => {
let index = this.right.sources.indexOf(this.video.iptv) + 1
if (index === this.right.sources.length) index = 0
stallCount++
clearTimeout(stallIptvTimeout)
if (stallCount >= this.right.sources.length) {
stallCount = 0
this.nextEvent()
} else {
this.playChannel(this.right.sources[index])
}
}, this.setting.waitingTimeInSec * 1000)
}
})
this.xg.on('canplay', () => {
stallCount = 0
if (stallIptvTimeout) clearTimeout(stallIptvTimeout)
})
this.xg.on('exitFullscreen', () => {
if (this.miniMode) this.xg.getCssFullscreen()
})
@@ -1360,6 +1448,7 @@ export default {
})
this.xg.on('timeupdate', () => {
if (this.isLive) return
const key = this.video.key + '@' + this.video.info.id
if (VIDEO_DETAIL_CACHE[key] && VIDEO_DETAIL_CACHE[key].endPosition) {
const time = this.xg.duration - VIDEO_DETAIL_CACHE[key].endPosition - this.xg.currentTime
@@ -1428,9 +1517,8 @@ export default {
this.videoStop()
return
}
var historyItem = this.right.history[0]
const historyItem = this.right.history[0]
this.video = { key: historyItem.site, info: { id: historyItem.ids, name: historyItem.name, index: historyItem.index } }
this.getUrls()
} else if (this.video.iptv && !this.isLive) {
this.playChannel(this.video.iptv)
this.state.showChannelList = false
@@ -1439,27 +1527,23 @@ export default {
})
},
videoStop () {
win.setProgressBar(-1)
if (this.xg.fullscreen) {
this.xg.exitFullscreen()
}
clearInterval(this.timer)
this.video.key = ''
this.xg.src = ''
this.config.url = ''
this.xg.destroy(false)
this.xg = null
this.name = ''
this.isLive = false
this.state.showChannelList = true
this.state.showTimespanSetting = false
this.right.list = []
this.getAllhistory()
setTimeout(() => {
this.xg = new HlsJsPlayer(this.config)
this.playerInstall()
this.bindEvent()
}, 100)
if (this.playerType !== 'flv') {
this.getPlayer(this.playerType, true)
} else {
this.xg.destroy()
this.getPlayer('hls', true)
}
},
minMaxEvent () {
win.on('minimize', () => {
@@ -1505,11 +1589,16 @@ export default {
})
},
showShortcutEvent () {
this.right.show = !this.right.show
shortcut.all().then(res => {
this.right.type = 'shortcut'
this.right.shortcut = res
})
if (this.right.type === 'shortcut') {
this.right.show = false
this.right.type = ''
} else {
this.right.show = true
shortcut.all().then(res => {
this.right.type = 'shortcut'
this.right.shortcut = res
})
}
}
},
created () {
@@ -1635,7 +1724,7 @@ export default {
top: 10px !important;
}
.xgplayer-skin-default .xgplayer-playbackrate ul {
bottom: 25px;
bottom: 20px;
}
.xgplayer-skin-default .xgplayer-playbackrate ul li {
font-size: 13px !important;
@@ -1685,6 +1774,7 @@ export default {
.right {
float: right;
svg {
display: inline-block;
margin-top: 8px;
cursor: pointer;
}

View File

@@ -1,6 +1,6 @@
<template>
<div class="listpage" id="recommendataions">
<div class="listpage-header" id="recommendataions-header">
<div class="listpage" id="recommendations">
<div class="listpage-header" id="recommendations-header">
<el-switch v-model="setting.recommendationViewMode" active-text="海报" active-value="picture" inactive-text="列表" inactive-value="table" @change="updateViewMode"></el-switch>
<el-button type="text">视频数{{ recommendations.length }}</el-button>
<el-select v-model="selectedAreas" size="small" multiple placeholder="地区" popper-class="popper" :popper-append-to-body="false">
@@ -29,10 +29,10 @@
</el-select>
<el-button :loading="loading" @click.stop="updateEvent" icon="el-icon-refresh">更新推荐</el-button>
</div>
<div class="listpage-body" id="recommendataions-body" >
<div class="listpage-body" id="recommendations-body" >
<div class="show-table" id="star-table" v-if="setting.recommendationViewMode === 'table'">
<el-table size="mini" fit height="100%" row-key="id"
ref="recommendataionsTable"
ref="recommendationsTable"
:data="filteredRecommendations"
@row-click="detailEvent">
<el-table-column
@@ -80,7 +80,7 @@
</el-table>
</div>
<div class="show-picture" id="star-picture" v-if="setting.recommendationViewMode === 'picture'">
<Waterfall ref="recommendataionsWaterfall" :list="filteredRecommendations" :gutter="20" :width="240"
<Waterfall ref="recommendationsWaterfall" :list="filteredRecommendations" :gutter="20" :width="240"
:breakpoints="{
1200: { //当屏幕宽度小于等于1200
rowPerView: 4,
@@ -100,7 +100,7 @@
<div class="rate" v-if="props.data.rate && props.data.rate !== '暂无评分'">
<span>{{props.data.rate}}</span>
</div>
<img style="width: 100%" :src="props.data.detail.pic" alt="" @load="$refs.recommendataionsWaterfall.refresh()" @click="detailEvent(props.data)">
<img style="width: 100%" :src="props.data.detail.pic" alt="" @load="$refs.recommendationsWaterfall.refresh()" @click="detailEvent(props.data)">
<div class="operate">
<div class="operate-wrap">
<span class="o-play" @click="playEvent(props.data)">播放</span>
@@ -190,7 +190,7 @@ export default {
}
},
filteredRecommendations () {
var filteredData = this.recommendations.filter(x => (this.selectedAreas.length === 0) || this.selectedAreas.includes(x.detail.area))
let filteredData = this.recommendations.filter(x => (this.selectedAreas.length === 0) || this.selectedAreas.includes(x.detail.area))
filteredData = filteredData.filter(x => (this.selectedTypes.length === 0) || this.selectedTypes.includes(x.detail.type))
return filteredData
}
@@ -198,7 +198,7 @@ export default {
watch: {
view () {
if (this.view === 'Recommendation') {
if (this.$refs.recommendataionsWaterfall) this.$refs.recommendataionsWaterfall.resize()
if (this.$refs.recommendationsWaterfall) this.$refs.recommendationsWaterfall.resize()
}
},
sortKeyword () {
@@ -279,31 +279,15 @@ export default {
info: e.detail
}
},
downloadEvent (e) {
const key = e.key
const id = e.ids
zy.download(key, id).then(res => {
if (res && res.m3u8List) {
const list = res.m3u8List.split('#')
let downloadUrl = ''
for (const i of list) {
const url = encodeURI(i.split('$')[1])
downloadUrl += (url + '\n')
}
clipboard.writeText(downloadUrl)
this.$message.success('『MP4』格式的链接已复制, 快去下载吧!')
} else {
zy.detail(key, id).then(res => {
const list = [...res.m3u8List]
let downloadUrl = ''
for (const i of list) {
const url = encodeURI(i.split('$')[1])
downloadUrl += (url + '\n')
}
clipboard.writeText(downloadUrl)
this.$message.success('『M3U8』格式的链接已复制, 快去下载吧!')
})
}
async downloadEvent (e) {
const db = await history.find({ site: e.key, ids: e.ids })
let videoFlag
if (db) videoFlag = db.videoFlag
zy.download(e.key, e.ids, videoFlag).then(res => {
clipboard.writeText(res.downloadUrls)
this.$message.success(res.info)
}).catch((err) => {
this.$message.error(err.info)
})
},
getRecommendations () {
@@ -319,7 +303,7 @@ export default {
this.areas = [...new Set(this.recommendations.map(ele => ele.detail.area))].filter(x => x)
},
updateViewMode () {
setTimeout(() => { if (this.$refs.recommendataionsWaterfall) this.$refs.recommendataionsWaterfall.refresh() }, 700)
setTimeout(() => { if (this.$refs.recommendationsWaterfall) this.$refs.recommendationsWaterfall.refresh() }, 700)
setting.find().then(res => {
res.recommendationViewMode = this.setting.recommendationViewMode
setting.update(res)
@@ -331,7 +315,7 @@ export default {
},
mounted () {
addEventListener('resize', () => {
setTimeout(() => { if (this.$refs.recommendataionsWaterfall) this.$refs.recommendataionsWaterfall.resize() }, 500)
setTimeout(() => { if (this.$refs.recommendationsWaterfall) this.$refs.recommendationsWaterfall.resize() }, 500)
})
}
}

View File

@@ -33,11 +33,11 @@
</div>
</div>
</div>
<div class="shortcut">
<div class="shortcut" title="清理缓存后图片资源需重新下载,不建议清理,软件会根据磁盘空间动态管理缓存大小">
<div class="title">缓存</div>
<div class="shortcut-box">
<div class="zy-select">
<div class="vs-placeholder vs-noAfter" @click="clearCache">清理视频缓存</div>
<div class="vs-placeholder vs-noAfter" @click="clearCache">清理缓存</div>
</div>
</div>
</div>
@@ -75,6 +75,10 @@
<div class="zy-input">
<input type="checkbox" v-model = "d.autocleanWhenIptvCheck" @change="updateSettingEvent"> 检测时自动清理无效源
</div>
<div class="zy-input">
<input type="checkbox" v-model = "d.autoChangeSourceWhenIptvStalling" @change="updateSettingEvent">
卡顿时自动换源换台:<input style="width:50px" type="number" min=0 v-model.number = "d.waitingTimeInSec" @change="updateSettingEvent">
</div>
</div>
</div>
<div class="site">
@@ -83,6 +87,9 @@
<div class="zy-select">
<div class="vs-placeholder vs-noAfter" @click="editSitesEvent">编辑源</div>
</div>
<div class="zy-select">
<div class="vs-placeholder vs-noAfter" @click="show.configDefaultParseUrlDialog = true">设置默认解析接口</div>
</div>
<div class="zy-input" @click="toggleExcludeRootClasses">
<input type="checkbox" v-model = "d.excludeRootClasses" @change="updateSettingEvent"> 屏蔽主分类
</div>
@@ -148,6 +155,20 @@
<span>所有资源来自网上, 该软件不参与任何制作, 上传, 储存等内容, 禁止传播违法资源. 该软件仅供学习参考, 请于安装后24小时内删除.</span>
</div>
</div>
<div> <!-- 设置默认解析接口 -->
<el-dialog :visible.sync="show.configDefaultParseUrlDialog" v-if='show.configDefaultParseUrlDialog' title="设置默认解析接口" :append-to-body="true" @close="closeDialog">
<el-form label-width="45px" label-position="left">
<el-form-item label="URL:" prop='defaultParseURL'>
<el-input v-model="setting.defaultParseURL" :autosize="{ minRows: 2, maxRows: 4}" type="textarea" placeholder="请输入解析接口地址,为空时会自动设置,重置时会自动更新默认接口地址"/>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="closeDialog">取消</el-button>
<el-button type="danger" @click="get7kParseURL">重置</el-button>
<el-button type="primary" @click="configDefaultParseURL">确定</el-button>
</span>
</el-dialog>
</div>
<div> <!-- 输入密码页面 -->
<el-dialog :visible.sync="show.checkPasswordDialog" v-if='show.checkPasswordDialog' :append-to-body="true" @close="closeDialog" width="300px">
<el-form label-width="75px" label-position="left">
@@ -205,14 +226,11 @@
<div class="wrapper">
<div class="body">
<div class="content" v-html="update.html"></div>
<div class="progress" v-show="update.percent > 0">
<el-progress :percentage="update.percent"></el-progress>
<div class="size" style="font-size: 14px">更新包大小: {{update.size}} KB</div>
</div>
</div>
<div class="footer">
<el-button size="small" @click="cancelUpdate">取消</el-button>
<el-button size="small" v-show="!update.downloaded" @click="startUpdate">更新</el-button>
<el-button size="small" @click="closeUpdate">关闭</el-button>
<el-button size="small" v-show="update.showDownload" @click="startUpdate">更新</el-button>
<el-button size="small" v-show="!update.showDownload && !update.downloaded">正在更新...</el-button>
<el-button size="small" v-show="update.downloaded" @click="installUpdate">安装</el-button>
</div>
</div>
@@ -241,7 +259,8 @@ export default {
checkPasswordDialog: false,
changePasswordDialog: false,
proxy: false,
proxyDialog: false
proxyDialog: false,
configDefaultParseUrlDialog: false
},
d: { },
latestVersion: pkg.version,
@@ -258,9 +277,8 @@ export default {
version: '',
show: false,
html: '',
percent: 0,
size: 0,
downloaded: false
downloaded: false,
showDownload: true
}
}
},
@@ -291,6 +309,7 @@ export default {
setting.find().then(res => {
this.d = res
this.setting = this.d
if (!this.setting.defaultParseURL) this.configDefaultParseURL()
})
},
getSites () {
@@ -327,6 +346,20 @@ export default {
this.d.excludeRootClasses = !this.d.excludeRootClasses
this.updateSettingEvent()
},
async get7kParseURL () {
this.$message.info('正在获取7K源解析地址...')
const parseURL = await zy.get7kParseURL()
if (parseURL.startsWith('http')) {
this.$message.success('获取成功,更新应用默认解析接口地址...')
this.setting.defaultParseURL = parseURL
}
},
async configDefaultParseURL () {
if (!this.setting.defaultParseURL) await this.get7kParseURL()
this.d.defaultParseURL = this.setting.defaultParseURL.trim()
this.show.configDefaultParseUrlDialog = false
this.updateSettingEvent()
},
selectLocalPlayer () {
const options = {
filters: [
@@ -337,7 +370,7 @@ export default {
}
remote.dialog.showOpenDialog(options).then(result => {
if (!result.canceled) {
var playerPath = result.filePaths[0].replace(/\\/g, '/')
const playerPath = result.filePaths[0].replace(/\\/g, '/')
this.$message.success('设定第三方播放器路径为:' + result.filePaths[0])
this.d.externalPlayer = playerPath
this.updateSettingEvent()
@@ -369,6 +402,7 @@ export default {
async closeDialog () {
this.show.checkPasswordDialog = false
this.show.changePasswordDialog = false
this.show.configDefaultParseUrlDialog = false
if (this.show.proxyDialog) {
this.show.proxyDialog = false
this.setting.proxy.type = 'none'
@@ -498,17 +532,15 @@ export default {
openUpdate () {
this.update.show = true
},
cancelUpdate () {
closeUpdate () {
this.update.show = false
},
startUpdate () {
this.update.showDownload = false
ipcRenderer.send('downloadUpdate')
ipcRenderer.on('download-progress', (info, progress) => {
this.update.size = progress.total
this.update.percent = parseFloat(progress.percent).toFixed(1)
})
ipcRenderer.on('update-downloaded', () => {
this.update.downloaded = true
this.$message.success('更新已下载完成Mac用户须手动点击“安装”其它系统会在退出后自动安装')
})
},
installUpdate () {

View File

@@ -1,7 +1,7 @@
<template>
<div class="share" id="share" @click="shareClickEvent" v-on-clickaway="shareClickEvent">
<div class="share" id="share" @click="shareClickEvent" v-clickoutside="shareClickEvent">
<div class="left">
<img :src="pic" alt="" @load="picLoadEvent">
<img :src="share.info.pic" alt="">
</div>
<div class="right" id="right">
<div class="title">{{ share.info.name }}</div>
@@ -22,7 +22,7 @@ import { mapMutations } from 'vuex'
import QrcodeVue from 'qrcode.vue'
import html2canvas from 'html2canvas'
import zy from '../lib/site/tools'
import { directive as onClickaway } from 'vue-clickaway'
import Clickoutside from 'element-ui/src/utils/clickoutside'
const { clipboard, nativeImage } = require('electron')
export default {
name: 'share',
@@ -45,6 +45,14 @@ export default {
set (val) {
this.SET_SHARE(val)
}
},
DetailCache: {
get () {
return this.$store.getters.getDetailCache
},
set (val) {
this.SET_DetailCache(val)
}
}
},
watch: {
@@ -58,42 +66,47 @@ export default {
}
},
directives: {
onClickaway: onClickaway
Clickoutside
},
methods: {
...mapMutations(['SET_SHARE']),
...mapMutations(['SET_SHARE', 'SET_DetailCache']),
shareClickEvent () {
this.share = {
show: false,
info: {}
}
},
async getUrl (dl) {
async getUrl (dl, index) {
const t = dl.dd._t
if (t) {
return t.split('#')[0].split('$')[1]
return t.split('#')[index].split('$')[1]
} else {
const id = this.share.info.ids || this.share.info.id
const res = await zy.detail(this.share.key, id)
const cacheKey = this.share.key + '@' + id
let res = this.DetailCache[cacheKey]
if (!this.DetailCache[cacheKey]) {
res = await zy.detail(this.share.key, id)
this.DetailCache[cacheKey] = res
}
if (res) {
return res.m3u8List[1]
return res.fullList[0].list[index]
}
}
},
async getDetail () {
this.loading = true
this.pic = this.share.info.pic
const url = await this.getUrl(this.share.info.dl)
const index = this.share.index || 0
const url = await this.getUrl(this.share.info.dl, index)
this.link = 'http://hunlongyu.gitee.io/zy-player-web?url=' + url + '&name=' + this.share.info.name
this.loading = false
},
picLoadEvent () {
const dom = document.getElementById('share')
html2canvas(dom).then(res => {
const png = res.toDataURL('image/png')
const p = nativeImage.createFromDataURL(png)
clipboard.writeImage(p)
this.$message.success('已复制到剪贴板,快去分享吧~ 严禁传播违法资源!!!')
this.$nextTick(() => {
const dom = document.getElementById('share')
html2canvas(dom, { useCORS: true }).then(res => {
const png = res.toDataURL('image/png')
const p = nativeImage.createFromDataURL(png)
clipboard.writeImage(p)
this.$message.success('已复制到剪贴板,快去分享吧~ 严禁传播违法资源!!!')
})
})
}
},

View File

@@ -107,7 +107,7 @@
<div class="update" v-if="props.data.hasUpdate">
<span>有更新</span>
</div>
<div class="progress" v-if="props.data.index && props.data.detail && props.data.detail.m3u8List !== undefined && props.data.detail.m3u8List.length > 1">
<div class="progress" v-if="props.data.index && props.data.detail && props.data.detail.fullList[0].list !== undefined && props.data.detail.fullList[0].list.length > 1">
<span>
看至第{{ props.data.index + 1 }}集
</span>
@@ -138,7 +138,7 @@
</template>
<script>
import { mapMutations } from 'vuex'
import { star, sites, setting } from '../lib/dexie'
import { history, star, sites, setting } from '../lib/dexie'
import zy from '../lib/site/tools'
import { remote } from 'electron'
import fs from 'fs'
@@ -201,6 +201,14 @@ export default {
set (val) {
this.SET_SETTING(val)
}
},
DetailCache: {
get () {
return this.$store.getters.getDetailCache
},
set (val) {
this.SET_DetailCache(val)
}
}
},
watch: {
@@ -269,9 +277,9 @@ export default {
},
async playEvent (e) {
if (e.index) {
this.video = { key: e.key, info: { id: e.ids, name: e.name, index: e.index }, detail: e.detail }
this.video = { key: e.key, info: { id: e.ids, name: e.name, index: e.index } }
} else {
this.video = { key: e.key, info: { id: e.ids, name: e.name, index: 0 }, detail: e.detail }
this.video = { key: e.key, info: { id: e.ids, name: e.name, index: 0 } }
}
if (e.hasUpdate) {
this.clearHasUpdateFlag(e)
@@ -306,21 +314,24 @@ export default {
this.getFavorites()
}
},
updateEvent (e) {
zy.detail(e.key, e.ids).then(detailRes => {
var doc = {
async updateEvent (e) {
try {
if (!this.DetailCache[e.key + '@' + e.ids]) {
this.DetailCache[e.key + '@' + e.ids] = await zy.detail(e.key, e.ids)
}
const doc = {
id: e.id,
key: e.key,
ids: e.ids,
site: e.site,
name: e.name,
detail: detailRes,
detail: this.DetailCache[e.key + '@' + e.ids],
index: e.index
}
star.get(e.id).then(resStar => {
if (!e.hasUpdate && e.detail.last !== detailRes.last) {
if (!e.hasUpdate && e.detail.last !== doc.detail.last) {
doc.hasUpdate = true
var msg = `同步"${e.name}"成功, 检查到更新。`
const msg = `同步"${e.name}"成功, 检查到更新。`
this.$message.success(msg)
} else {
this.numNoUpdate += 1
@@ -328,10 +339,10 @@ export default {
star.update(e.id, doc)
this.getFavorites()
})
}).catch(err => {
var msg = `同步"${e.name}"失败, 请重试`
} catch (err) {
const msg = `同步"${e.name}"失败, 请重试`
this.$message.warning(msg, err)
})
}
},
updateAllEvent () {
this.numNoUpdate = 0
@@ -339,38 +350,22 @@ export default {
this.updateEvent(e)
})
},
downloadEvent (e) {
const key = e.key
const id = e.id
zy.download(key, id).then(res => {
if (res && res.m3u8List) {
const list = res.m3u8List.split('#')
let downloadUrl = ''
for (const i of list) {
const url = encodeURI(i.split('$')[1])
downloadUrl += (url + '\n')
}
clipboard.writeText(downloadUrl)
this.$message.success('『MP4』格式的链接已复制, 快去下载吧!')
} else {
zy.detail(key, id).then(res => {
const list = [...res.m3u8List]
let downloadUrl = ''
for (const i of list) {
const url = encodeURI(i.split('$')[1])
downloadUrl += (url + '\n')
}
clipboard.writeText(downloadUrl)
this.$message.success('『M3U8』格式的链接已复制, 快去下载吧!')
})
}
async downloadEvent (e) {
const db = await history.find({ site: e.key, ids: e.ids })
let videoFlag
if (db) videoFlag = db.videoFlag
zy.download(e.key, e.ids, videoFlag).then(res => {
clipboard.writeText(res.downloadUrls)
this.$message.success(res.info)
}).catch((err) => {
this.$message.error(err.info)
})
},
getSiteName (row) {
if (row.site) {
return row.site.name
} else {
var site = this.sites.find(e => e.key === row.key)
const site = this.sites.find(e => e.key === row.key)
if (site) {
return site.name
}
@@ -422,15 +417,25 @@ export default {
}
remote.dialog.showOpenDialog(options).then(result => {
if (!result.canceled) {
var starList = Array.from(this.list)
var id = this.list.length + 1
const starList = Array.from(this.list)
let id = this.list.length + 1
result.filePaths.forEach(file => {
var str = fs.readFileSync(file)
const str = fs.readFileSync(file)
const json = JSON.parse(str)
json.reverse().forEach(ele => {
const starExists = starList.some(x => x.key === ele.key && x.ids === ele.ids)
if (!starExists) {
var doc = {
const newDetail = {
director: ele.director,
actor: ele.actor,
type: ele.type,
area: ele.area,
lang: ele.lang,
year: ele.year,
last: ele.last,
note: ele.note
}
const doc = {
id: id,
key: ele.key,
ids: ele.ids,
@@ -439,16 +444,7 @@ export default {
hasUpdate: ele.hasUpdate,
index: ele.index,
rate: ele.rate,
detail: ele.detail === undefined ? {
director: ele.director,
actor: ele.actor,
type: ele.type,
area: ele.area,
lang: ele.lang,
year: ele.year,
last: ele.last,
note: ele.note
} : ele.detail
detail: ele.detail === undefined ? newDetail : ele.detail
}
id += 1
starList.push(doc)
@@ -472,7 +468,7 @@ export default {
updateDatabase () {
this.syncTableData()
star.clear().then(res => {
var id = this.list.length
let id = this.list.length
this.list.forEach(ele => {
ele.id = id
id -= 1
@@ -481,6 +477,7 @@ export default {
})
},
rowDrop () {
if (!document.getElementById('star-table')) return
const tbody = document.getElementById('star-table').querySelector('.el-table__body-wrapper tbody')
const _this = this
Sortable.create(tbody, {

View File

@@ -16,6 +16,7 @@ db.version(4).stores({
channelList: '++id, name, prefer, channels, group, isActive'
})
// 开发和稳定版同一版本号会有不同的数据库
// 参考https://github.com/dfahlander/Dexie.js/releases/tag/v3.0.0-alpha.3 upgrade可以改变主键和表名了
// https://dexie.org/docs/Version/Version.stores()
// https://dexie.org/docs/Version/Version.upgrade()
@@ -30,6 +31,34 @@ db.version(6).stores({
await tx.shortcut.bulkAdd(localKey)
})
db.version(7).stores({
sites: '++id, key, name, api, download, jiexiUrl, isActive, group',
history: '++id, [site+ids], name, type, year, index, time, duration, detail, onlinePlay'
}).upgrade(trans => {
trans.sites.toCollection().modify(site => {
site.jiexiUrl = ''
})
trans.history.toCollection().modify(record => {
record.detail.fullList = [].concat(record.detail.m3u8List)
delete record.detail.m3u8List
})
trans.star.toCollection().modify(favorite => {
favorite.detail.fullList = [].concat(favorite.detail.m3u8List)
delete favorite.detail.m3u8List
})
})
db.version(8).stores({
}).upgrade(trans => {
trans.sites.toCollection().modify(site => {
if (site.api.includes('7kjx.com')) site.jiexiUrl = 'default'
})
trans.setting.toCollection().modify(setting => {
setting.waitingTimeInSec = 15
setting.autoChangeSourceWhenIptvStalling = true
})
})
db.on('populate', () => {
db.setting.bulkAdd(setting)
db.sites.bulkAdd(sites)

View File

@@ -5,9 +5,9 @@
"name": "麻花资源",
"api": "http://www.mhapi123.com/inc/ldg_api.php",
"download": "",
"jiexiUrl": "",
"group": "默认",
"isActive": true,
"status": "可用"
"isActive": true
},
{
"id": 2,
@@ -15,9 +15,9 @@
"name": "1886 资源",
"api": "http://cj.1886zy.co/inc/api.php",
"download": "",
"jiexiUrl": "",
"group": "默认",
"isActive": true,
"status": "可用"
"isActive": true
},
{
"id": 3,
@@ -25,138 +25,158 @@
"name": "123 资源",
"api": "http://cj.123ku2.com:12315/inc/api.php",
"download": "",
"jiexiUrl": "",
"group": "默认",
"isActive": true,
"status": "可用"
"isActive": true
},
{
"key": "7K资源",
"id": 4,
"name": "7K资源",
"api": "https://zy.7kjx.com/cjapi.php",
"download": "",
"jiexiUrl": "default",
"group": "默认",
"isActive": true
},
{
"key": "1717云资源网",
"id": 5,
"name": "1717云资源网",
"api": "http://zy.itono.cn/inc/api.php",
"download": "",
"jiexiUrl": "https://www.1717yun.com/jiexi/?url=",
"group": "默认",
"isActive": true
},
{
"id": 6,
"key": "subo988",
"name": "速播资源站",
"api": "https://www.subo988.com/inc/api.php",
"download": "",
"jiexiUrl": "",
"group": "默认",
"isActive": true,
"status": "可用"
"isActive": true
},
{
"id": 5,
"id": 7,
"key": "88zyw",
"name": "88 影视资源站",
"api": "http://www.88zyw.net/inc/api.php",
"download": "",
"jiexiUrl": "",
"group": "默认",
"isActive": true,
"status": "可用"
"isActive": true
},
{
"key": "zuidazy",
"id": 6,
"id": 8,
"name": "最大资源网",
"api": "http://www.zdziyuan.com/inc/ldg_sea.php",
"download": "http://www.zdziyuan.com/inc/apidown.php",
"jiexiUrl": "",
"group": "默认",
"isActive": true,
"status": "可用"
"isActive": true
},
{
"key": "mbo",
"id": 7,
"id": 9,
"name": "秒播资源",
"api": "http://caiji.mb77.vip/inc/seacmsapi.php",
"download": "",
"jiexiUrl": "",
"group": "默认",
"isActive": true,
"status": "可用"
"isActive": true
},
{
"id": 8,
"id": 10,
"key": "apibdzy",
"name": "百度云资源",
"api": "https://api.apibdzy.com/api.php/provide/vod/at/xml",
"download": "",
"jiexiUrl": "",
"group": "默认",
"isActive": true,
"status": "可用"
"isActive": true
},
{
"id": 9,
"id": 11,
"key": "okzy",
"name": "OK 资源网",
"api": "http://cj.okzy.tv/inc/api.php",
"download": "http://cj.okzy.tv/inc/apidown.php",
"jiexiUrl": "",
"group": "默认",
"isActive": true,
"status": "可用"
"isActive": true
},
{
"id": 10,
"id": 12,
"key": "kuyunzy",
"name": "酷云资源",
"api": "http://caiji.kuyun98.com/inc/ldg_api.php",
"download": "http://caiji.kuyun98.com/inc/apidown.php",
"jiexiUrl": "",
"group": "默认",
"isActive": true,
"status": "可用"
"isActive": true
},
{
"id": 11,
"id": 13,
"key": "kubozy",
"name": "酷播资源",
"api": "http://api.kbzyapi.com/inc/api.php",
"download": "",
"jiexiUrl": "",
"group": "默认",
"isActive": true,
"status": "可用"
"isActive": true
},
{
"id": 12,
"id": 14,
"key": "yongjiuzy",
"name": "永久资源",
"api": "http://cj.yongjiuzyw.com/inc/api.php",
"download": "",
"jiexiUrl": "",
"group": "默认",
"isActive": true,
"status": "可用"
"isActive": true
},
{
"id": 13,
"id": 15,
"key": "rrzy",
"name": "人人资源",
"api": "https://www.rrzyw.cc/api.php/provide/vod/from/rrm3u8/at/xml/",
"download": "",
"jiexiUrl": "",
"group": "默认",
"isActive": true,
"status": "可用"
"isActive": true
},
{
"id": 14,
"id": 16,
"key": "bbkdj",
"name": "步步高顶尖资源网",
"api": "http://api.bbkdj.com/api",
"download": "",
"jiexiUrl": "",
"group": "默认",
"isActive": true,
"status": "可用"
"isActive": true
},
{
"id": 15,
"id": 17,
"key": "zuixinzy",
"name": "最新资源",
"api": "http://api.zuixinapi.com/inc/api.php",
"download": "",
"jiexiUrl": "",
"group": "默认",
"isActive": true,
"status": "可用"
"isActive": true
},
{
"id": 16,
"id": 18,
"key": "209zy",
"name": "209 资源",
"api": "http://cj.1156zy.com/inc/api.php",
"download": "",
"jiexiUrl": "",
"group": "默认",
"isActive": true,
"status": "可用"
"isActive": true
}
]
]

View File

@@ -9,6 +9,7 @@ const setting = [
excludeRootClasses: true,
excludeR18Films: true,
forwardTimeInSec: 5,
waitingTimeInSec: 15,
starViewMode: 'picture',
recommendationViewMode: 'picture',
historyViewMode: 'picture',
@@ -21,7 +22,8 @@ const setting = [
port: ''
},
allowPassWhenIptvCheck: true,
autocleanWhenIptvCheck: false
autocleanWhenIptvCheck: false,
autoChangeSourceWhenIptvStalling: true
}
]

View File

@@ -3,36 +3,55 @@ import axios from 'axios'
import parser from 'fast-xml-parser'
import cheerio from 'cheerio'
import { Parser as M3u8Parser } from 'm3u8-parser'
// import FLVDemuxer from 'xgplayer-flv.js/src/flv/demux/flv-demuxer.js'
import SocksProxyAgent from 'socks-proxy-agent'
// axios使用系统代理 https://evandontje.com/2020/04/02/automatic-system-proxy-configuration-for-electron-applications/
// xgplayer使用chromium代理设置浏览器又默认使用系统代理 https://www.chromium.org/developers/design-documents/network-settings
// 要在设置中添加代理设置可参考https://stackoverflow.com/questions/37393248/how-connect-to-proxy-in-electron-webview
var http = require('http')
var https = require('http')
const http = require('http')
const https = require('http')
const { remote } = require('electron')
var win = remote.getCurrentWindow()
var session = win.webContents.session
var ElectronProxyAgent = require('electron-proxy-agent')
const win = remote.getCurrentWindow()
const session = win.webContents.session
const ElectronProxyAgent = require('electron-proxy-agent')
const URL = require('url')
const request = require('request')
let proxyURL
// 取消axios请求 浅析cancelToken https://juejin.cn/post/6844904168277147661 https://masteringjs.io/tutorials/axios/cancel
// const source = axios.CancelToken.source()
// const cancelToken = source.token
// 请求超时时限
axios.defaults.timeout = 10000 // 可能使用代理,增长超时
// axios.defaults.timeout = 10000 // 可能使用代理,增长超时
const TIMEOUT = 20000
// 重试次数,共请求3
axios.defaults.retry = 2
// 重试次数,共请求2
axios.defaults.retry = 1
// 请求的间隙
axios.defaults.retryDelay = 1000
// 使用请求拦截器动态调整超时
axios.interceptors.request.use(function (config) {
if (config.__retryCount === undefined) {
config.timeout = TIMEOUT
} else {
config.timeout = TIMEOUT * (config.__retryCount + 1)
}
return config
}, function (err) {
return Promise.reject(err)
})
// 添加响应拦截器
axios.interceptors.response.use(function (response) {
// 对响应数据做些事
if (response.status && response.status === 200 && response.request.responseURL.includes('api.php') && !response.data.startsWith('<?xml')) {
}
return response
}, function (err) { // 请求错误时做些事
// 请求超时的之后,抛出 err.code = ECONNABORTED的错误..错误信息是 timeout of xxx ms exceeded
if (err.code === 'ECONNABORTED' && err.message.indexOf('timeout') !== -1) {
var config = err.config
const config = err.config
config.__retryCount = config.__retryCount || 0
if (config.__retryCount >= config.retry) {
@@ -42,7 +61,7 @@ axios.interceptors.response.use(function (response) {
config.__retryCount += 1
var backoff = new Promise(function (resolve) {
const backoff = new Promise(function (resolve) {
setTimeout(function () {
resolve()
}, config.retryDelay || 1)
@@ -139,7 +158,11 @@ const zy = {
const json = parser.parse(data, this.xmlConfig)
const jsondata = json.rss === undefined ? json : json.rss
const videoList = jsondata.list.video
resolve(videoList)
if (videoList && videoList.length) {
resolve(videoList)
} else {
resolve([])
}
}).catch(err => {
reject(err)
})
@@ -163,7 +186,7 @@ const zy = {
url = `${site.api}?ac=videolist`
}
axios.post(url).then(async res => {
const data = res.data
const data = res.data.match(/<list [^>]*>/)[0] + '</list>' // 某些源站不含页码时获取到的数据parser无法解析
const json = parser.parse(data, this.xmlConfig)
const jsondata = json.rss === undefined ? json : json.rss
const pg = {
@@ -190,7 +213,7 @@ const zy = {
this.getSite(key).then(res => {
const site = res
wd = encodeURI(wd)
var url = `${site.api}?wd=${wd}`
const url = `${site.api}?wd=${wd}`
axios.post(url, { timeout: 3000 }).then(res => {
const data = res.data
const json = parser.parse(data, this.xmlConfig)
@@ -222,21 +245,50 @@ const zy = {
const json = parser.parse(data, this.xmlConfig)
const jsondata = json.rss === undefined ? json : json.rss
const videoList = jsondata.list.video
// Parse m3u8List
var m3u8List = []
// Parse video lists
let fullList = []
let index = 0
const supportedFormats = ['m3u8', 'mp4']
const dd = videoList.dl.dd
const type = Object.prototype.toString.call(dd)
if (type === '[object Array]') {
for (const i of dd) {
// 如果含有多个视频列表的话, 仅获取m3u8列表
if (i._flag.includes('m3u8')) {
m3u8List = i._t.split('#')
i._t = i._t.replace(/\$+/g, '$')
const ext = Array.from(new Set(...i._t.split('#').map(e => e.includes('$') ? e.split('$')[1].match(/\.\w+?$/) : e.match(/\.\w+?$/)))).map(e => e.slice(1))
if (ext.length && ext.length <= supportedFormats.length && ext.every(e => supportedFormats.includes(e))) {
if (ext.length === 1) {
i._flag = ext[0]
} else {
i._flag = index ? 'ZY支持-' + index : 'ZY支持'
index++
}
}
fullList.push(
{
flag: i._flag,
list: i._t.split('#').filter(e => e && (e.startsWith('http') || (e.split('$')[1] && e.split('$')[1].startsWith('http'))))
}
)
}
} else {
m3u8List = dd._t.split('#')
fullList.push(
{
flag: dd._flag,
list: dd._t.replace(/\$+/g, '$').split('#').filter(e => e && (e.startsWith('http') || (e.split('$')[1] && e.split('$')[1].startsWith('http'))))
}
)
}
videoList.m3u8List = m3u8List
fullList.forEach(item => {
if (item.list.every(e => e.includes('$') && /^\s*\d+\s*$/.test(e.split('$')[0]))) item.list.sort((a, b) => { return a.split('$')[0] - b.split('$')[0] })
})
if (fullList.length > 1) { // 将ZY支持的播放列表前置
index = fullList.findIndex(e => supportedFormats.includes(e.flag) || e.flag.startsWith('ZY支持'))
if (index !== -1) {
const first = fullList.splice(index, 1)
fullList = first.concat(fullList)
}
}
videoList.fullList = fullList
resolve(videoList)
}).catch(err => {
reject(err)
@@ -252,8 +304,10 @@ const zy = {
* @param {*} id 资源唯一标识符 id
* @returns
*/
download (key, id) {
download (key, id, videoFlag) {
return new Promise((resolve, reject) => {
let info = ''
let downloadUrls = ''
this.getSite(key).then(res => {
const site = res
if (site.download) {
@@ -263,24 +317,42 @@ const zy = {
const json = parser.parse(data, this.xmlConfig)
const jsondata = json.rss === undefined ? json : json.rss
const videoList = jsondata.list.video
// Parse m3u8List
var m3u8List = []
const dd = videoList.dl.dd
const type = Object.prototype.toString.call(dd)
if (type === '[object Array]') {
for (const i of dd) {
m3u8List = i._t.split('#')
downloadUrls = i._t.replace(/\$+/g, '$').split('#').map(e => encodeURI(e.includes('$') ? e.split('$')[1] : e)).join('\n')
}
} else {
m3u8List = dd._t.split('#')
downloadUrls = dd._t.replace(/\$+/g, '$').split('#').map(e => encodeURI(e.includes('$') ? e.split('$')[1] : e)).join('\n')
}
videoList.m3u8List = m3u8List
resolve(videoList)
}).catch(err => {
if (downloadUrls) {
info = '调用下载接口获取到的链接已复制, 快去下载吧!'
resolve({ downloadUrls: downloadUrls, info: info })
} else {
throw new Error()
}
}).catch((err) => {
err.info = '无法获取到下载链接,请通过播放页面点击“调试”按钮获取'
reject(err)
})
} else {
resolve([])
zy.detail(key, id).then(res => {
const dl = res.fullList.find(e => e.flag === videoFlag) || res.fullList[0]
for (const i of dl.list) {
const url = encodeURI(i.includes('$') ? i.split('$')[1] : i)
downloadUrls += (url + '\n')
}
if (downloadUrls) {
info = '视频源链接已复制, 快去下载吧!'
resolve({ downloadUrls: downloadUrls, info: info })
} else {
throw new Error()
}
}).catch((err) => {
err.info = '无法获取到下载链接,请通过播放页面点击“调试”按钮获取'
reject(err)
})
}
})
})
@@ -307,49 +379,72 @@ const zy = {
* @param {*} channel 直播频道 url
* @returns boolean
*/
async checkChannel (channel) {
checkChannel (url) {
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)
const supportFormats = /\.(m3u8|flv)$/
const extRE = url.match(supportFormats) || new URL.URL(url).pathname.match(supportFormats)
if (extRE[1] === 'flv') {
const MAX_CONTENT_LENGTH = 2000 // axios配置maxContentLength不生效先用request凑合
let receivedLength = 0
let options = { uri: url, gzip: true, timeout: 10000 }
if (proxyURL) {
if (proxyURL.startsWith('http')) options = Object.assign({ proxy: proxyURL }, options)
if (proxyURL.startsWith('socks5')) options = Object.assign({ agent: new SocksProxyAgent(proxyURL) }, options)
}
}).catch(e => {
resolve(false)
})
const req = request.get(options)
.on('data', (str) => {
receivedLength += str.length
if (receivedLength > MAX_CONTENT_LENGTH) {
resolve(true) // 应该用FLVDemuxer.probe来检测先凑合
req.abort()
}
})
.on('error', function (err) {
resolve(false)
console.log(err)
})
.on('end', () => { resolve(false) })
} else if (extRE[1] === 'm3u8') {
axios.get(url).then(res => {
const manifest = res.data
const parser = new M3u8Parser()
parser.push(manifest)
parser.end()
const parsedManifest = parser.manifest
if (parsedManifest.segments.length) {
resolve(true)
} else {
resolve(false)
}
}).catch(e => {
resolve(false)
})
}
})
},
/**
* 获取豆瓣页面链接
* @param {*} name 视频名称
* @param {*} year 视频年份
* @returns 豆瓣页面链接,如果没有搜到该视频,返回搜索页面链接
*/
doubanLink (name) {
doubanLink (name, year) {
return new Promise((resolve, reject) => {
// 豆瓣搜索链接
var nameToSearch = name.replace(/\s/g, '')
var doubanSearchLink = 'https://www.douban.com/search?q=' + nameToSearch
const nameToSearch = name.replace(/\s/g, '')
const doubanSearchLink = 'https://www.douban.com/search?q=' + nameToSearch
axios.get(doubanSearchLink).then(res => {
const $ = cheerio.load(res.data)
// 比较第一和第二给豆瓣搜索结果, 看名字是否相符
var link = ''
var linkInDouban = $($('div.result')[0]).find('div>div>h3>a').first()
var nameInDouban = linkInDouban.text().replace(/\s/g, '')
if (nameToSearch === nameInDouban) {
link = linkInDouban.attr('href')
} else {
linkInDouban = $($('div.result')[1]).find('div>div>h3>a').first()
nameInDouban = linkInDouban.text().replace(/\s/g, '')
if (nameToSearch === nameInDouban) {
// 查询所有搜索结果, 看名字和年代是否相符
let link = ''
$('div.result').each(function () {
const linkInDouban = $(this).find('div>div>h3>a').first()
const nameInDouban = linkInDouban.text().replace(/\s/g, '')
const subjectCast = $(this).find('span.subject-cast').text()
if (nameToSearch === nameInDouban && subjectCast && subjectCast.includes(year)) {
link = linkInDouban.attr('href')
}
}
})
if (link) {
resolve(link)
} else {
@@ -364,18 +459,19 @@ const zy = {
/**
* 获取豆瓣评分
* @param {*} name 视频名称
* @param {*} year 视频年份
* @returns 豆瓣评分
*/
doubanRate (name) {
doubanRate (name, year) {
return new Promise((resolve, reject) => {
var nameToSearch = name.replace(/\s/g, '')
this.doubanLink(nameToSearch).then(link => {
const nameToSearch = name.replace(/\s/g, '')
this.doubanLink(nameToSearch, year).then(link => {
if (link.includes('https://www.douban.com/search')) {
resolve('暂无评分')
} else {
axios.get(link).then(response => {
const parsedHtml = cheerio.load(response.data)
var rating = parsedHtml('body').find('#interest_sectl').first().find('strong').first()
const rating = parsedHtml('body').find('#interest_sectl').first().find('strong').first()
if (rating.text()) {
resolve(rating.text().replace(/\s/g, ''))
} else {
@@ -390,16 +486,26 @@ const zy = {
})
})
},
async proxy () {
get7kParseURL () {
return new Promise((resolve, reject) => {
axios.get('https://zy.7kjx.com/').then(res => {
const $ = cheerio.load(res.data)
const parseURL = $('body > div.container > div > div.stui-pannel > div.col-pd > p:contains("解析接口:")').first().find('a').text()
resolve(parseURL)
}).catch(err => { reject(err) })
})
},
proxy () {
return new Promise((resolve, reject) => {
setting.find().then(db => {
if (db && db.proxy && db.proxy.type === 'manual') {
if (db.proxy.scheme && db.proxy.url && db.proxy.port) {
const proxyURL = db.proxy.scheme + '://' + db.proxy.url.trim() + ':' + db.proxy.port.trim()
proxyURL = db.proxy.scheme + '://' + db.proxy.url.trim() + ':' + db.proxy.port.trim()
session.setProxy({ proxyRules: proxyURL })
http.globalAgent = https.globalAgent = new ElectronProxyAgent(session)
}
} else {
proxyURL = ''
session.setProxy({ proxyRules: 'direct://' })
http.globalAgent = https.globalAgent = new ElectronProxyAgent(session)
}

View File

@@ -1,9 +1,10 @@
import { BrowserWindow, ipcMain } from 'electron'
const { autoUpdater } = require('@imjs/electron-differential-updater')
const { autoUpdater } = require('electron-updater')
// electron-updater 增量更新时似乎无法显示进度
export function initUpdater (win = BrowserWindow) {
autoUpdater.autoDownload = false
autoUpdater.autoInstallOnAppQuit = false
autoUpdater.autoInstallOnAppQuit = true
// 主进程监听检查更新事件
ipcMain.on('checkForUpdate', () => {
@@ -41,8 +42,8 @@ export function initUpdater (win = BrowserWindow) {
})
// 下载更新进度
autoUpdater.on('download-progress', (info, progress) => {
win.webContents.send('download-progress', info, progress)
autoUpdater.on('download-progress', (progressObj) => {
win.webContents.send('download-progress', progressObj)
})
// 下载完成

View File

@@ -28,7 +28,8 @@ export default new Vuex.Store({
},
appState: {
windowIsOnTop: false
}
},
DetailCache: {}
},
getters: {
getView: state => {
@@ -48,6 +49,9 @@ export default new Vuex.Store({
},
getAppState: state => {
return state.appState
},
getDetailCache: state => {
return state.DetailCache
}
},
mutations: {
@@ -68,6 +72,9 @@ export default new Vuex.Store({
},
SET_APPSTATE: (state, payload) => {
state.appState = payload
},
set_DetailCache: (state, payload) => {
state.DetailCache = payload
}
}
})

663
yarn.lock

File diff suppressed because it is too large Load Diff