diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx
index 0dd1017..da6141f 100644
--- a/app/dashboard/page.tsx
+++ b/app/dashboard/page.tsx
@@ -26,18 +26,10 @@ import { useBiliUsers } from '../lib/use-streamers'
import styles from '../styles/dashboard.module.scss'
// 注册各平台组件
+import plugins from '../ui/plugins'
import Global from '../ui/plugins/global'
-import Cookie from '../ui/plugins/cookie'
-import Bilibili from '../ui/plugins/bilibili'
-import CC from '../ui/plugins/cc'
-import Douyin from '../ui/plugins/douyin'
-import Douyu from '../ui/plugins/douyu'
-import Huya from '../ui/plugins/huya'
-import Twitch from '../ui/plugins/twitch'
-import YouTube from '../ui/plugins/youtube'
import Developer from '../ui/plugins/developer'
-import Twitcasting from '@/app/ui/plugins/twitcasting'
-import Kilakila from '@/app/ui/plugins/kilakila'
+
const Dashboard: React.FC = () => {
const { Header, Content } = Layout
@@ -196,26 +188,12 @@ const Dashboard: React.FC = () => {
各平台下载设置
- {/* 哔哩哔哩 */}
-
- {/* CC直播 */}
-
- {/* 抖音 */}
-
- {/* 斗鱼 */}
-
- {/* 虎牙 */}
-
- {/* 克拉克拉 */}
-
- {/* 老鼠台 */}
-
- {/* Twitcasting */}
-
- {/* 油管 */}
-
- {/* 饼干 */}
-
+ {Object.entries(plugins)
+ .filter(([key]) => key !== 'Cookie')
+ .map(([key, Plugin]) => (
+
+ ))}
+
diff --git a/app/lib/api-streamer.ts b/app/lib/api-streamer.ts
index 305b2f0..147e385 100644
--- a/app/lib/api-streamer.ts
+++ b/app/lib/api-streamer.ts
@@ -107,6 +107,7 @@ export interface LiveStreamerEntity {
downloaded_processor?: Record<'run', string>[];
postprocessor?: (Record<'run' | 'mv', string> | 'rm')[];
opt_args?: string[];
+ override?: Record;
}
export interface BiliType {
diff --git a/app/streamers/page.tsx b/app/streamers/page.tsx
index 3ccb960..dfbf2a9 100644
--- a/app/streamers/page.tsx
+++ b/app/streamers/page.tsx
@@ -15,11 +15,13 @@ import {
IconVideoListStroked,
IconEdit2Stroked,
IconDeleteStroked,
+ IconWrench,
} from '@douyinfe/semi-icons'
import { List, ButtonGroup } from '@douyinfe/semi-ui'
import React, { useState } from 'react'
import useStreamers from '../lib/use-streamers'
import TemplateModal from '../ui/TemplateModal'
+import OverrideModal from '../ui/OverrideModal'
import { LiveStreamerEntity, put, requestDelete, sendRequest } from '../lib/api-streamer'
import useSWRMutation from 'swr/mutation'
@@ -92,7 +94,7 @@ export default function Home() {
}
const handleUpdate = async (values: any) => {
- // console.log(values);
+ console.log(values);
delete values.status
if (values?.postprocessor) {
values.postprocessor = values.postprocessor.map(
@@ -246,6 +248,10 @@ export default function Home() {
}>
+
+ }>
+
+
Promise
+}
+
+const removeCircularReferences = (obj: any, seen = new WeakSet()): any => {
+ // 处理 null 或非对象类型
+ if (obj === null || typeof obj !== 'object') return obj
+
+ // 检测循环引用
+ if (seen.has(obj)) return '[Circular Reference]'
+ seen.add(obj)
+
+ if (Array.isArray(obj)) {
+ return obj.map((item: any) => removeCircularReferences(item, seen))
+ }
+
+ const result: Record = {}
+ for (const [key, value] of Object.entries(obj)) {
+ // 跳过 React 相关的属性
+ if (key === '_context' || key === 'Provider' || key === 'Consumer') continue
+ result[key] = removeCircularReferences(value, seen)
+ }
+ return result
+}
+
+const OverrideModal: React.FC = ({ children, entity, onOk }) => {
+ const [isOpen, setOpen] = useState(false)
+
+ const toggle = () => {
+ setOpen(!isOpen)
+ }
+
+ const platformSetting = () => {
+ for (const [pattern, Plugin] of Object.entries(SupportedPlatforms)) {
+ if (entity?.url.match(new RegExp(pattern))) {
+ // console.log('匹配到平台:', pattern)
+ return Plugin as React.ComponentType
+ }
+ }
+ // console.log('未匹配到平台')
+ return null
+ }
+
+ const api = useRef()
+
+ const { biliUsers } = useBiliUsers()
+ const list = biliUsers?.map(item => {
+ return {
+ value: item.value,
+ label: (
+ <>
+
+ {item.name}
+ >
+ ),
+ }
+ })
+
+ const [visible, setVisible] = useState(false)
+ const showDialog = () => {
+ setVisible(true)
+ }
+ const handleOk = async () => {
+ let values = await api.current?.validate()
+ // 从 LiveStreamerEntity 接口定义中获取所有字段
+ const entityFields = new Set([
+ 'id',
+ 'url',
+ 'remark',
+ 'filename',
+ 'split_time',
+ 'split_size',
+ 'upload_id',
+ 'status',
+ 'format',
+ 'time_range',
+ 'preprocessor',
+ 'segment_processor',
+ 'downloaded_processor',
+ 'postprocessor',
+ 'opt_args',
+ 'override',
+ ])
+
+ if (values) {
+ // 处理 override_text
+ if (values.override_text) {
+ try {
+ values.override = JSON.parse(values.override_text)
+ delete values.override_text
+ } catch (e) {
+ Notification.error({
+ title: '错误',
+ content: '配置格式不正确,请检查 JSON 格式',
+ })
+ return
+ }
+ }
+
+ const overrideConfig = { ...(values.override || {}) }
+ Object.keys(values).forEach(key => {
+ console.log(key, values[key])
+ if (!entityFields.has(key)) {
+ if (values[key] != undefined && values[key] != null) {
+ overrideConfig[key] = values[key]
+ }
+ delete values[key]
+ }
+ })
+ values.override = overrideConfig
+
+ // 处理循环引用
+ const cleanValues = removeCircularReferences(values)
+ await onOk(cleanValues)
+ setVisible(false)
+ return
+ }
+ setVisible(false)
+ }
+ const handleCancel = () => {
+ setVisible(false)
+ }
+
+ const childrenWithProps = React.Children.map(children, child => {
+ if (React.isValidElement(child)) {
+ return React.cloneElement(child, {
+ onClick: () => {
+ showDialog()
+ child.props.onClick?.()
+ },
+ })
+ }
+ })
+
+ const downloadSettings = (
+
+
+
+ streamlink(hls多线程下载)
+ ffmpeg
+ stream-gears(默认)
+ sync-downloader(边录边传)
+
+
+
+
+
+
+
+
+
+
+ )
+
+ return (
+ <>
+ {childrenWithProps}
+
+ {
+ if (!value) return true
+ try {
+ JSON.parse(value)
+ return true
+ } catch (e) {
+ return false
+ }
+ },
+ message: '请输入有效的 JSON 格式',
+ },
+ ]}
+ />
+
+
+ {downloadSettings}
+ {(() => {
+ const Plugin = platformSetting()
+ return Plugin ? (
+
+ ) : null
+ })()}
+
+
+
+
+ >
+ )
+}
+
+export default OverrideModal
diff --git a/app/ui/TemplateFields.tsx b/app/ui/TemplateFields.tsx
index b7a5b47..7ec78b3 100644
--- a/app/ui/TemplateFields.tsx
+++ b/app/ui/TemplateFields.tsx
@@ -183,36 +183,7 @@ const TemplateFields: React.FC>
setSelectedMinutes(Math.floor(minutes / 5) * 5)
}
}
- }, [formApi]);
-
- // useEffect(() => {
- // const extraFieldsList = formApi.getValue('extra_fields_list') as Array<{key: string; value: string}>;
- // if (!extraFieldsList?.length) return;
-
- // const extraFieldsObj = extraFieldsList.reduce((tempObj: Record, item) => {
- // if (item?.key && item?.value !== undefined) {
- // tempObj[item.key] = !isNaN(Number(item.value)) ? Number(item.value) : item.value;
- // }
- // return tempObj;
- // }, {});
- // formApi.setValue('extra_fields', JSON.stringify(extraFieldsObj));
- // }, []);
-
- // useEffect(() => {
- // const extraFields = formApi.getValue('extra_fields');
- // if (extraFields) {
- // try {
- // const parsedFields = JSON.parse(extraFields);
- // const fieldsList = Object.entries(parsedFields).map(([key, value]) => ({
- // key,
- // value: String(value)
- // }));
- // formApi.setValue('extra_fields_list', fieldsList);
- // } catch (e) {
- // console.error('解析 extra_fields 失败:', e);
- // }
- // }
- // }, []);
+ }, [formApi])
return (
<>
@@ -344,68 +315,6 @@ const TemplateFields: React.FC>
}
]}
/>
- {/* {
- const extraFields = formApi.getValue('extra_fields');
- if (!extraFields) return [];
- try {
- const parsedFields = JSON.parse(extraFields);
- return Object.entries(parsedFields).map(([key, value]) => ({
- key,
- value: String(value)
- }));
- } catch (e) {
- console.error('解析 extra_fields 失败:', e);
- return [];
- }
- })()}
- >
- {({ add, arrayFields }) => (
-
- } theme='light'>Add new line
- {
- arrayFields.map(({ field, key, remove }, i) => (
-
- {
- if (!value) return true;
- const currentFields = (formApi.getValue('extra_fields_list') || []) as Array<{key: string; value: string}>;
- const keyCount = currentFields.filter(
- (item: any, index: number) =>
- item.key === value &&
- `extra_fields_list.${index}.key` !== field + '.key'
- ).length;
- if (keyCount > 1) {
- throw new Error('键名不能重复');
- }
- return true;
- }
- }
- ]}
- />
-
- }
- onClick={remove}
- style={{ margin: 12 }}
- />
-
- ))}
-
- )}
- */}
= ({ children, entity, onOk })
-
-
}
const Bilibili: React.FC = props => {
- const entity = props.entity
- const list = props.list
+ const { entity, list, initValues } = props
+ const formApi = useFormApi()
+
+ useEffect(() => {
+ if (initValues) {
+ Object.entries(initValues).forEach(([key, value]) => {
+ formApi.setValue(key, value)
+ })
+ }
+ }, [initValues, formApi])
+
return (
<>
diff --git a/app/ui/plugins/cc.tsx b/app/ui/plugins/cc.tsx
index dfab5ca..34e0aa0 100644
--- a/app/ui/plugins/cc.tsx
+++ b/app/ui/plugins/cc.tsx
@@ -1,8 +1,25 @@
'use client'
-import React from 'react'
-import { Form, Select, Collapse } from '@douyinfe/semi-ui'
+import React, { useEffect } from 'react'
+import { Form, Select, Collapse, useFormApi } from '@douyinfe/semi-ui'
+
+type Props = {
+ entity: any
+ list: any
+ initValues?: Record
+}
+
+const CC: React.FC = props => {
+ const { entity, list, initValues } = props
+ const formApi = useFormApi()
+
+ useEffect(() => {
+ if (initValues) {
+ Object.entries(initValues).forEach(([key, value]) => {
+ formApi.setValue(key, value)
+ })
+ }
+ }, [initValues, formApi])
-const CC: React.FC = () => {
return (
<>
diff --git a/app/ui/plugins/douyin.tsx b/app/ui/plugins/douyin.tsx
index 363e05a..34842b7 100644
--- a/app/ui/plugins/douyin.tsx
+++ b/app/ui/plugins/douyin.tsx
@@ -1,13 +1,25 @@
'use client'
-import React from 'react'
-import { Form, Select, Collapse } from '@douyinfe/semi-ui'
+import React, { useEffect } from 'react'
+import { Form, Select, Collapse, useFormApi } from '@douyinfe/semi-ui'
type Props = {
entity: any
+ list: any
+ initValues?: Record
}
const Douyin: React.FC = props => {
- const entity = props.entity
+ const { entity, list, initValues } = props
+ const formApi = useFormApi()
+
+ useEffect(() => {
+ if (initValues) {
+ Object.entries(initValues).forEach(([key, value]) => {
+ formApi.setValue(key, value)
+ })
+ }
+ }, [initValues, formApi])
+
return (
<>
diff --git a/app/ui/plugins/douyu.tsx b/app/ui/plugins/douyu.tsx
index e2b5b86..a61b66c 100644
--- a/app/ui/plugins/douyu.tsx
+++ b/app/ui/plugins/douyu.tsx
@@ -1,8 +1,25 @@
'use client'
-import React from 'react'
-import { Form, Select, Collapse } from '@douyinfe/semi-ui'
+import React, { useEffect } from 'react'
+import { Form, Select, Collapse, useFormApi } from '@douyinfe/semi-ui'
+
+type Props = {
+ entity: any
+ list: any
+ initValues?: Record
+}
+
+const Douyu: React.FC = props => {
+ const { entity, list, initValues } = props
+ const formApi = useFormApi()
+
+ useEffect(() => {
+ if (initValues) {
+ Object.entries(initValues).forEach(([key, value]) => {
+ formApi.setValue(key, value)
+ })
+ }
+ }, [initValues, formApi])
-const Douyu: React.FC = () => {
return (
<>
diff --git a/app/ui/plugins/huya.tsx b/app/ui/plugins/huya.tsx
index a38ed51..4bc5c71 100644
--- a/app/ui/plugins/huya.tsx
+++ b/app/ui/plugins/huya.tsx
@@ -1,13 +1,25 @@
'use client'
-import React from 'react'
-import { Form, Select, Collapse } from '@douyinfe/semi-ui'
+import React, { useEffect } from 'react'
+import { Form, Select, Collapse, useFormApi } from '@douyinfe/semi-ui'
type Props = {
entity: any
+ list: any
+ initValues?: Record
}
const Huya: React.FC = props => {
- const entity = props.entity
+ const { entity, list, initValues } = props
+ const formApi = useFormApi()
+
+ useEffect(() => {
+ if (initValues) {
+ Object.entries(initValues).forEach(([key, value]) => {
+ formApi.setValue(key, value)
+ })
+ }
+ }, [initValues, formApi])
+
return (
<>
diff --git a/app/ui/plugins/index.ts b/app/ui/plugins/index.ts
new file mode 100644
index 0000000..9899ea1
--- /dev/null
+++ b/app/ui/plugins/index.ts
@@ -0,0 +1,52 @@
+import Bilibili from './bilibili'
+import CC from './cc'
+import Cookie from './cookie'
+import Douyin from './douyin'
+import Douyu from './douyu'
+import Huya from './huya'
+import Kilakila from './kilakila'
+import Twitcasting from './twitcasting'
+import Twitch from './twitch'
+import Youtube from './youtube'
+
+export {
+ Bilibili,
+ CC,
+ Cookie,
+ Douyin,
+ Douyu,
+ Huya,
+ Kilakila,
+ Twitcasting,
+ Twitch,
+ Youtube,
+ SupportedPlatforms,
+}
+
+// 导出所有插件
+const plugins = {
+ Bilibili,
+ CC,
+ Cookie,
+ Douyin,
+ Douyu,
+ Huya,
+ Kilakila,
+ Twitcasting,
+ Twitch,
+ Youtube,
+}
+
+const SupportedPlatforms = {
+ 'https?:\/\/(b23\.tv|live\.bilibili\.com)': Bilibili,
+ 'https?:\/\/(cc\.163\.com)': CC,
+ 'https?:\/\/(?:(?:www|m|live|v)\.)?douyin\.com': Douyin,
+ 'https?:\/\/(?:(?:www|m)\.)?douyu\.com': Douyu,
+ 'https?:\/\/(?:(?:www|m)\.)?huya\.com': Huya,
+ 'https?:\/\/(live\.kilakila\.cn|www\.hongdoufm\.com)': Kilakila,
+ 'https?:\/\/twitcasting\.tv': Twitcasting,
+ 'https?:\/\/(?:(?:www|go|m)\.)?twitch\.tv': Twitch,
+ 'https?:\/\/(?:(?:www|m)\.)?youtube\.com': Youtube,
+}
+
+export default plugins
diff --git a/app/ui/plugins/kilakila.tsx b/app/ui/plugins/kilakila.tsx
index f69c3e8..f8df11d 100644
--- a/app/ui/plugins/kilakila.tsx
+++ b/app/ui/plugins/kilakila.tsx
@@ -1,12 +1,25 @@
'use client'
-import React from 'react'
-import { Form, Select, Collapse } from '@douyinfe/semi-ui'
+import React, { useEffect } from 'react'
+import { Form, Select, Collapse, useFormApi } from '@douyinfe/semi-ui'
type Props = {
entity: any
+ list: any
+ initValues?: Record
}
-const Kilakila: React.FC = () => {
+const Kilakila: React.FC = props => {
+ const { entity, list, initValues } = props
+ const formApi = useFormApi()
+
+ useEffect(() => {
+ if (initValues) {
+ Object.entries(initValues).forEach(([key, value]) => {
+ formApi.setValue(key, value)
+ })
+ }
+ }, [initValues, formApi])
+
return (
<>
diff --git a/app/ui/plugins/twitcasting.tsx b/app/ui/plugins/twitcasting.tsx
index 07d00ba..9c331d2 100644
--- a/app/ui/plugins/twitcasting.tsx
+++ b/app/ui/plugins/twitcasting.tsx
@@ -1,8 +1,25 @@
'use client'
-import React from 'react'
-import { Form, Collapse } from '@douyinfe/semi-ui'
+import React, { useEffect } from 'react'
+import { Form, Select, Collapse, useFormApi } from '@douyinfe/semi-ui'
+
+type Props = {
+ entity: any
+ list: any
+ initValues?: Record
+}
+
+const TwitCasting: React.FC = props => {
+ const { entity, list, initValues } = props
+ const formApi = useFormApi()
+
+ useEffect(() => {
+ if (initValues) {
+ Object.entries(initValues).forEach(([key, value]) => {
+ formApi.setValue(key, value)
+ })
+ }
+ }, [initValues, formApi])
-const TwitCasting: React.FC = () => {
return (
<>
diff --git a/app/ui/plugins/twitch.tsx b/app/ui/plugins/twitch.tsx
index 8fcb7b5..71a895f 100644
--- a/app/ui/plugins/twitch.tsx
+++ b/app/ui/plugins/twitch.tsx
@@ -1,13 +1,25 @@
'use client'
-import React from 'react'
-import { Form, Select, Collapse } from '@douyinfe/semi-ui'
+import React, { useEffect } from 'react'
+import { Form, Select, Collapse, useFormApi } from '@douyinfe/semi-ui'
type Props = {
entity: any
+ list: any
+ initValues?: Record
}
const Twitch: React.FC = props => {
- const entity = props.entity
+ const { entity, list, initValues } = props
+ const formApi = useFormApi()
+
+ useEffect(() => {
+ if (initValues) {
+ Object.entries(initValues).forEach(([key, value]) => {
+ formApi.setValue(key, value)
+ })
+ }
+ }, [initValues, formApi])
+
return (
<>
diff --git a/app/ui/plugins/youtube.tsx b/app/ui/plugins/youtube.tsx
index 5c2440d..82bcb66 100644
--- a/app/ui/plugins/youtube.tsx
+++ b/app/ui/plugins/youtube.tsx
@@ -1,13 +1,25 @@
'use client'
-import React from 'react'
-import { Form, Select, Collapse } from '@douyinfe/semi-ui'
+import React, { useEffect } from 'react'
+import { Form, Select, Collapse, useFormApi } from '@douyinfe/semi-ui'
type Props = {
entity: any
+ list: any
+ initValues?: Record
}
const YouTube: React.FC = props => {
- const entity = props.entity
+ const { entity, list, initValues } = props
+ const formApi = useFormApi()
+
+ useEffect(() => {
+ if (initValues) {
+ Object.entries(initValues).forEach(([key, value]) => {
+ formApi.setValue(key, value)
+ })
+ }
+ }, [initValues, formApi])
+
return (
<>
diff --git a/biliup/database/models.py b/biliup/database/models.py
index bfade42..c182b95 100644
--- a/biliup/database/models.py
+++ b/biliup/database/models.py
@@ -147,6 +147,7 @@ class LiveStreamers(BaseModel):
upload_streamers_id = mapped_column(ForeignKey("uploadstreamers.id", ondelete="CASCADE"), nullable=True)
uploadstreamers: Mapped[UploadStreamers] = relationship(back_populates="livestreamers")
format: Mapped[str] = mapped_column(nullable=True) # 视频格式
+ override = mapped_column(JSON(), nullable=True) # 覆写配置
preprocessor = mapped_column(JSON(), nullable=True) # 开始下载直播时触发
segment_processor = mapped_column(JSON(), nullable=True) # 分段时触发
downloaded_processor = mapped_column(JSON(), nullable=True) # 准备上传直播时触发
diff --git a/biliup/downloader.py b/biliup/downloader.py
index 837c1f3..d09d13f 100644
--- a/biliup/downloader.py
+++ b/biliup/downloader.py
@@ -9,16 +9,31 @@ logger = logging.getLogger('biliup')
def download(fname, url, **kwargs):
pg = None
+ override = kwargs.get('override', {})
for plugin in Plugin.download_plugins:
if re.match(plugin.VALID_URL_BASE, url):
pg = plugin(fname, url)
for k in pg.__dict__:
if kwargs.get(k):
pg.__dict__[k] = kwargs.get(k)
- break
if not pg:
pg = general.__plugin__(fname, url)
logger.warning(f'Not found plugin for {fname} -> {url} This may cause problems')
+ if override:
+ if pg.__class__ in Plugin.download_plugins:
+ # 单独适配的plugin允许全覆写
+ pg.__dict__.update(override)
+ if override.get('user'):
+ pg.__dict__.pop('user')
+ pg.__dict__.update(override.get('user'))
+ else:
+ # print("Override General plugin")
+ # 通用插件只允许覆写插件存在的值
+ for k in pg.__dict__:
+ if k in override:
+ pg.__dict__[k] = override[k]
+ # print(override)
+ del override
return pg.start()
diff --git a/biliup/engine/download.py b/biliup/engine/download.py
index af72556..f5cbd83 100644
--- a/biliup/engine/download.py
+++ b/biliup/engine/download.py
@@ -343,6 +343,8 @@ class DownloadBase(ABC):
ret = self.run()
except Exception:
logger.warning(f"{self.plugin_msg}: 下载失败", exc_info=True)
+ # TODO: 重试等待时间
+ time.sleep(3)
finally:
self.close()
diff --git a/biliup/plugins/bilibili.py b/biliup/plugins/bilibili.py
index 867f81f..a366736 100644
--- a/biliup/plugins/bilibili.py
+++ b/biliup/plugins/bilibili.py
@@ -14,7 +14,7 @@ from ..engine.download import DownloadBase
OFFICIAL_API = "https://api.live.bilibili.com"
STREAM_NAME_REGEXP = r"/live-bvc/\d+/(live_[^/\.]+)"
-@Plugin.download(regexp=r'(?:https?://)?(b23\.tv|live\.bilibili\.com)')
+@Plugin.download(regexp=r'https?://(b23\.tv|live\.bilibili\.com)')
class Bililive(DownloadBase):
def __init__(self, fname, url, suffix='flv'):
super().__init__(fname, url, suffix)
@@ -23,19 +23,22 @@ class Bililive(DownloadBase):
self.bilibili_danmaku_detail = config.get('bilibili_danmaku_detail', False)
self.__real_room_id = None
self.__login_mid = 0
- if config.get('user', {}).get('bili_cookie'):
- self.fake_headers['cookie'] = config.get('user', {}).get('bili_cookie')
- if config.get('user', {}).get('bili_cookie_file'):
- cookie_file_name = config.get('user', {}).get('bili_cookie_file')
- try:
- with open(cookie_file_name, encoding='utf-8') as stream:
- cookies = json.load(stream)["cookie_info"]["cookies"]
- cookies_str = ''
- for i in cookies:
- cookies_str += f"{i['name']}={i['value']};"
- self.fake_headers['cookie'] = cookies_str
- except Exception:
- logger.exception("load_cookies error")
+ self.bili_cookie = config.get('user', {}).get('bili_cookie')
+ self.bili_cookie_file = config.get('user', {}).get('bili_cookie_file')
+ self.bili_qn = int(config.get('bili_qn', 20000))
+ self.bili_protocol = config.get('bili_protocol', 'stream')
+ self.bili_cdn = config.get('bili_cdn', [])
+ self.bili_hls_timeout = config.get('bili_hls_transcode_timeout', 60)
+ self.bili_api_list = [
+ normalize_url(config.get('bili_liveapi', OFFICIAL_API)).rstrip('/'),
+ normalize_url(config.get('bili_fallback_api', OFFICIAL_API)).rstrip('/'),
+ ]
+ self.bili_force_source = config.get('bili_force_source', False)
+ self.bili_anonymous_origin = config.get('bili_anonymous_origin', False)
+ self.bili_ov2cn = config.get('bili_ov2cn', False)
+ self.bili_normalize_cn204 = config.get('bili_normalize_cn204', False)
+ self.cn01_sids = config.get('bili_replace_cn01', [])
+ self.bili_cdn_fallback = config.get('bili_cdn_fallback', False)
async def acheck_stream(self, is_check=False):
@@ -53,7 +56,18 @@ class Bililive(DownloadBase):
return False
room_id = match1(self.url, r'bilibili.com/(\d+)')
- quality_number = int(config.get('bili_qn', 10000))
+ if self.bili_cookie:
+ self.fake_headers['cookie'] = self.bili_cookie
+ if self.bili_cookie_file:
+ try:
+ with open(self.bili_cookie_file, encoding='utf-8') as stream:
+ cookies = json.load(stream)["cookie_info"]["cookies"]
+ cookies_str = ''
+ for i in cookies:
+ cookies_str += f"{i['name']}={i['value']};"
+ self.fake_headers['cookie'] = cookies_str
+ except Exception:
+ logger.exception("load_cookies error")
self.fake_headers['referer'] = self.url
# 获取直播状态与房间标题
@@ -93,7 +107,7 @@ class Bililive(DownloadBase):
# 复用原画 m3u8 流
if self.raw_stream_url is not None \
and ".m3u8" in self.raw_stream_url \
- and quality_number >= 10000 \
+ and self.bili_qn >= 10000 \
and not is_new_live:
url = await self.acheck_url_healthy(self.raw_stream_url)
if url is not None:
@@ -102,36 +116,26 @@ class Bililive(DownloadBase):
else:
self.raw_stream_url = None
- protocol = config.get('bili_protocol', 'stream')
- perf_cdn = config.get('bili_perfCDN')
- perf_cdn = config.get('bili_cdn', perf_cdn.split(',') if isinstance(perf_cdn, str) else [])
- self.__hls_timeout = config.get('bili_hls_transcode_timeout', 60)
- normalize_url = lambda a: a if a.startswith(('http://', 'https://')) else 'http://' + a
- self.__api_list = [
- normalize_url(config.get('bili_liveapi', OFFICIAL_API)).rstrip('/'),
- normalize_url(config.get('bili_fallback_api', OFFICIAL_API)).rstrip('/'),
- ]
-
- stream_urls = await self.aget_stream(quality_number, protocol, special_type)
+ stream_urls = await self.aget_stream(self.bili_qn, self.bili_protocol, special_type)
if not stream_urls:
- if protocol == 'hls_fmp4':
- if int(time.time()) - live_start_time <= self.__hls_timeout:
+ if self.bili_protocol == 'hls_fmp4':
+ if int(time.time()) - live_start_time <= self.bili_hls_timeout:
logger.warning(f"{self.plugin_msg}: 暂未提供 hls_fmp4 流,等待下一次检测")
return False
else:
# 回退首个可用格式
- stream_urls = await self.aget_stream(quality_number, 'stream', special_type)
+ stream_urls = await self.aget_stream(self.bili_qn, 'stream', special_type)
else:
- logger.error(f"{self.plugin_msg}: 获取{protocol}流失败")
+ logger.error(f"{self.plugin_msg}: 获取{self.bili_protocol}流失败")
return False
target_quality_stream = stream_urls.get(
- quality_number, next(iter(stream_urls.values()))
+ str(self.bili_qn), next(iter(stream_urls.values()))
)
stream_url = {}
- if perf_cdn is not None:
- for cdn in perf_cdn:
+ if self.bili_cdn is not None:
+ for cdn in self.bili_cdn:
stream_info = target_quality_stream.get(cdn)
if stream_info is not None:
current_cdn = cdn
@@ -142,15 +146,15 @@ class Bililive(DownloadBase):
stream_url = stream_info['url']
logger.debug(f"{self.plugin_msg}: 使用 {current_cdn} 流")
- self.raw_stream_url = normalize_cn204(
+ self.raw_stream_url = self.normalize_cn204(
f"{stream_url['host']}{stream_url['base_url']}{stream_url['extra']}"
)
# 替换 cn-gotcha01 节点
- cn01_sids = config.get('bili_replace_cn01', [])
- if cn01_sids and "cn-gotcha01" in current_cdn:
- if isinstance(cn01_sids, str): cn01_sids = cn01_sids.split(',')
- for sid in cn01_sids:
+ if self.cn01_sids and "cn-gotcha01" in current_cdn:
+ if isinstance(self.cn01_sids, str):
+ self.cn01_sids = self.cn01_sids.split(',')
+ for sid in self.cn01_sids:
new_url = f"https://{sid}.bilivideo.com{stream_url['base_url']}{stream_url['extra']}"
new_url = await self.acheck_url_healthy(new_url)
if new_url is not None:
@@ -160,13 +164,13 @@ class Bililive(DownloadBase):
logger.debug(f"{self.plugin_msg}: {sid} is not available")
# 强制原画
- if quality_number <= 10000 and config.get('bili_force_source'):
+ if self.bili_qn <= 10000 and self.bili_force_source:
# 不处理 qn 20000
if stream_info['suffix'] not in {'origin', 'uhd', 'maxhevc'}:
__base_url = stream_url['base_url'].replace(f"_{stream_info['suffix']}", "")
__sk = match1(stream_url['extra'], r'sk=([^&]+)')
__extra = stream_url['extra'].replace(__sk, __sk[:32])
- __url = normalize_cn204(f"{stream_url['host']}{__base_url}{__extra}")
+ __url = self.normalize_cn204(f"{stream_url['host']}{__base_url}{__extra}")
if (await self.acheck_url_healthy(__url)) is not None:
self.raw_stream_url = __url
logger.info(f"{self.plugin_msg}: force_source 处理 {current_cdn} 成功 {stream_info['stream_name']}")
@@ -175,13 +179,13 @@ class Bililive(DownloadBase):
f"{self.plugin_msg}: force_source 处理 {current_cdn} 失败 {stream_info['stream_name']}"
)
-
- if config.get('bili_cdn_fallback'):
+ # 回退
+ if self.bili_cdn_fallback:
__url = await self.acheck_url_healthy(self.raw_stream_url)
if __url is None:
for cdn, stream_info in target_quality_stream.items():
stream_url = stream_info['url']
- __fallback_url = normalize_cn204(
+ __fallback_url = self.normalize_cn204(
f"{stream_url['host']}{stream_url['base_url']}{stream_url['extra']}"
)
try:
@@ -256,7 +260,7 @@ class Bililive(DownloadBase):
full_url, params=params, headers=self.fake_headers
)
if m3u8_res.status_code == 200 and m3u8_res.text.startswith("#EXTM3U"):
- return parse_master_m3u8(m3u8_res.text)
+ return self.parse_master_m3u8(m3u8_res.text)
except Exception as e:
logger.error(f"{self.plugin_msg}: {api} 获取 m3u8 失败 -> {e}", exc_info=True)
return {}
@@ -269,14 +273,14 @@ class Bililive(DownloadBase):
:return: 流信息
"""
stream_urls = {}
- for api in self.__api_list:
+ for api in self.bili_api_list:
play_info = await self.get_play_info(api, qn)
if not play_info or check_areablock(play_info):
# logger.error(f"{self.plugin_msg}: {api} 返回内容错误: {play_info}")
continue
streams = play_info['playurl_info']['playurl']['stream']
if protocol == 'hls_fmp4':
- if config.get('bili_anonymous_origin'):
+ if self.bili_anonymous_origin:
if special_type in play_info['all_special_types'] and not self.__login_mid:
logger.warn(f"{self.plugin_msg}: 特殊直播{special_type}")
else:
@@ -287,14 +291,14 @@ class Bililive(DownloadBase):
stream = streams[1] if len(streams) > 1 else streams[0]
for format in stream['format']:
if format['format_name'] == 'fmp4':
- stream_urls = parse_stream_url(format['codec'][0])
+ stream_urls = self.parse_stream_url(format['codec'][0])
# fmp4 可能没有原画
if qn == 10000 and qn in stream_urls.keys():
break
else:
stream_urls = {}
else:
- stream_urls = parse_stream_url(streams[0]['format'][0]['codec'][0])
+ stream_urls = self.parse_stream_url(streams[0]['format'][0]['codec'][0])
if stream_urls:
break
# 空字典照常返回,重试交给上层方法处理
@@ -317,6 +321,84 @@ class Bililive(DownloadBase):
logger.error(f"{self.plugin_msg}: 登录态校验失败 {e}")
return 0
+ def normalize_cn204(self, url: str) -> str:
+ if self.bili_normalize_cn204 and "cn-gotcha204" in url:
+ return re.sub(r"(?<=cn-gotcha204)-[1-4]", "", url, 1)
+ return url
+
+ def parse_stream_url(self, *args) -> dict:
+ suffix_regexp = r'suffix=([^&]+)'
+ if isinstance(args[0], str):
+ url = args[0]
+ host = "https://" + match1(url, r'https?://([^/]+)')
+ stream_url = {
+ 'host': host if not self.bili_ov2cn else host.replace("ov-", "cn-"),
+ 'base_url': url.split("?")[0].split(host)[1] + "?",
+ 'extra': url.split("?")[1]
+ }
+ return {
+ 'url': stream_url,
+ 'stream_name': match1(url, STREAM_NAME_REGEXP),
+ 'suffix': match1(url, suffix_regexp)
+ }
+ elif isinstance(args[0], dict):
+ streams = {}
+ current_qn = args[0]['current_qn']
+ streams.setdefault(current_qn, {})
+ base_url = args[0]['base_url']
+ for info in args[0]['url_info']:
+ cdn_name = match1(info['extra'], r'cdn=([^&]+)')
+ stream_url = {
+ 'host': info['host'] if not self.bili_ov2cn else info['host'].replace("ov-", "cn-"),
+ 'base_url': base_url,
+ 'extra': info['extra']
+ }
+ streams[current_qn].setdefault(cdn_name, {
+ 'url': stream_url,
+ 'stream_name': match1(base_url, STREAM_NAME_REGEXP),
+ 'suffix': match1(info['extra'], suffix_regexp)
+ })
+ return streams
+
+
+ def parse_master_m3u8(self, m3u8_content: str) -> dict:
+ """
+ Returns:
+ {
+ "qn值": {
+ "cdn名称": {
+ "url": parsed_stream_url,
+ "stream_name": "流名称",
+ "suffix": "二压后缀"
+ }
+ }
+ }
+ """
+ lines = m3u8_content.strip().splitlines()
+ current_qn = None
+ result = {}
+
+ if not lines[0].startswith('#EXTM3U'):
+ raise ValueError('Invalid m3u8 file')
+
+ for line in lines:
+ if line.startswith('#EXT-X-STREAM-INF:'):
+ codec = match1(line, r'CODECS="([^"]+)"')
+ current_qn = match1(line, r'BILI-QN=(\d+)')
+
+ if codec and current_qn:
+ if 'avc' in codec.lower():
+ result.setdefault(current_qn, {})
+ else:
+ current_qn = None
+
+ elif line.startswith('http') and current_qn is not None:
+ cdn_name = match1(line, r'cdn=([^&]+)')
+ if cdn_name:
+ result[current_qn].setdefault(cdn_name, self.parse_stream_url(line))
+
+ return dict(sorted(result.items(), key=lambda x: int(x[0]), reverse=True))
+
# Copy from room-player.js
def check_areablock(data):
'''
@@ -328,82 +410,5 @@ def check_areablock(data):
return True
return False
-def normalize_cn204(url: str) -> str:
- if config.get('bili_normalize_cn204') and "cn-gotcha204" in url:
- return re.sub(r"(?<=cn-gotcha204)-[1-4]", "", url, 1)
- return url
-
-
-def parse_stream_url(*args) -> dict:
- ov2cn = config.get('bili_ov2cn', False)
- suffix_regexp = r'suffix=([^&]+)'
- if isinstance(args[0], str):
- url = args[0]
- host = "https://" + match1(url, r'https?://([^/]+)')
- stream_url = {
- 'host': host if not ov2cn else host.replace("ov-", "cn-"),
- 'base_url': url.split("?")[0].split(host)[1] + "?",
- 'extra': url.split("?")[1]
- }
- return {
- 'url': stream_url,
- 'stream_name': match1(url, STREAM_NAME_REGEXP),
- 'suffix': match1(url, suffix_regexp)
- }
- elif isinstance(args[0], dict):
- streams = {}
- current_qn = args[0]['current_qn']
- streams.setdefault(current_qn, {})
- base_url = args[0]['base_url']
- for info in args[0]['url_info']:
- cdn_name = match1(info['extra'], r'cdn=([^&]+)')
- stream_url = {
- 'host': info['host'] if not ov2cn else info['host'].replace("ov-", "cn-"),
- 'base_url': base_url,
- 'extra': info['extra']
- }
- streams[current_qn].setdefault(cdn_name, {
- 'url': stream_url,
- 'stream_name': match1(base_url, STREAM_NAME_REGEXP),
- 'suffix': match1(info['extra'], suffix_regexp)
- })
- return streams
-
-
-def parse_master_m3u8(m3u8_content: str) -> dict:
- """
- Returns:
- {
- "qn值": {
- "cdn名称": {
- "url": parsed_stream_url,
- "stream_name": "流名称",
- "suffix": "二压后缀"
- }
- }
- }
- """
- lines = m3u8_content.strip().splitlines()
- current_qn = None
- result = {}
-
- if not lines[0].startswith('#EXTM3U'):
- raise ValueError('Invalid m3u8 file')
-
- for line in lines:
- if line.startswith('#EXT-X-STREAM-INF:'):
- codec = match1(line, r'CODECS="([^"]+)"')
- current_qn = match1(line, r'BILI-QN=(\d+)')
-
- if codec and current_qn:
- if 'avc' in codec.lower():
- result.setdefault(current_qn, {})
- else:
- current_qn = None
-
- elif line.startswith('http') and current_qn is not None:
- cdn_name = match1(line, r'cdn=([^&]+)')
- if cdn_name:
- result[current_qn].setdefault(cdn_name, parse_stream_url(line))
-
- return dict(sorted(result.items(), key=lambda x: int(x[0]), reverse=True))
\ No newline at end of file
+def normalize_url(url: str) -> str:
+ return url if url.startswith(('http://', 'https://')) else 'http://' + url
\ No newline at end of file
diff --git a/biliup/plugins/cc.py b/biliup/plugins/cc.py
index fdb5cac..2834ba3 100644
--- a/biliup/plugins/cc.py
+++ b/biliup/plugins/cc.py
@@ -6,10 +6,11 @@ from ..engine.decorators import Plugin
from ..engine.download import DownloadBase
-@Plugin.download(regexp=r'(?:https?://)?cc\.163\.com')
+@Plugin.download(regexp=r'https?://cc\.163\.com')
class CC(DownloadBase):
def __init__(self, fname, url, suffix='flv'):
super().__init__(fname, url, suffix)
+ self.cc_protocol = config.get('cc_protocol', 'hls')
async def acheck_stream(self, is_check=False):
rid = match1(self.url, r"(\d{4,})")
@@ -33,7 +34,7 @@ class CC(DownloadBase):
headers=self.fake_headers
)).json()["data"][0]
self.room_title = channel_info["title"]
- if config.get("cc_protocol", "hls") == "hls":
+ if self.cc_protocol == "hls":
self.raw_stream_url = channel_info["sharefile"]
else:
original = {"vbr": 0}
diff --git a/biliup/plugins/douyin.py b/biliup/plugins/douyin.py
index e013c5b..d8c7152 100644
--- a/biliup/plugins/douyin.py
+++ b/biliup/plugins/douyin.py
@@ -13,7 +13,7 @@ from ..engine.decorators import Plugin
from ..engine.download import DownloadBase
-@Plugin.download(regexp=r'(?:https?://)?(?:(?:www|m|live|v)\.)?douyin\.com')
+@Plugin.download(regexp=r'https?://(?:(?:www|m|live|v)\.)?douyin\.com')
class Douyin(DownloadBase):
def __init__(self, fname, url, suffix='flv'):
super().__init__(fname, url, suffix)
@@ -21,6 +21,9 @@ class Douyin(DownloadBase):
self.fake_headers['user-agent'] = DouyinUtils.DOUYIN_USER_AGENT
self.fake_headers['referer'] = "https://live.douyin.com/"
self.fake_headers['cookie'] = config.get('user', {}).get('douyin_cookie', '')
+ self.douyin_quality = config.get('douyin_quality', 'origin')
+ self.douyin_protocol = config.get('douyin_protocol', 'hls')
+ self.douyin_double_screen = config.get('douyin_double_screen', False)
self.__web_rid = None # 网页端房间号 或 抖音号
self.__room_id = None # 单场直播的直播房间
self.__sec_uid = None
@@ -81,6 +84,8 @@ class Douyin(DownloadBase):
_room_info = await self.get_web_room_info(self.__web_rid)
if _room_info:
if not _room_info['data'].get('user'):
+ if _room_info['data'].get('prompts', '') == '直播已结束':
+ return False
# 可能是用户被封禁
raise Exception(f"{str(_room_info)}")
self.__sec_uid = _room_info['data']['user']['sec_uid']
@@ -111,7 +116,7 @@ class Douyin(DownloadBase):
try:
pull_data = room_info['stream_url']['live_core_sdk_data']['pull_data']
- if room_info['stream_url'].get('pull_datas') and config.get('douyin_double_screen', False):
+ if room_info['stream_url'].get('pull_datas') and self.douyin_double_screen:
pull_data = next(iter(room_info['stream_url']['pull_datas'].values()))
stream_data = json.loads(pull_data['stream_data'])['data']
except:
@@ -120,7 +125,7 @@ class Douyin(DownloadBase):
# 原画origin 蓝光uhd 超清hd 高清sd 标清ld 流畅md 仅音频ao
quality_items = ['origin', 'uhd', 'hd', 'sd', 'ld', 'md']
- quality = config.get('douyin_quality', 'origin')
+ quality = self.douyin_quality
if quality not in quality_items:
quality = quality_items[0]
try:
@@ -151,7 +156,7 @@ class Douyin(DownloadBase):
else:
quality = optional_quality_items[optional_quality_index - 1]
- protocol = 'hls' if config.get('douyin_protocol') == 'hls' else 'flv'
+ protocol = 'hls' if self.douyin_protocol == 'hls' else 'flv'
self.raw_stream_url = stream_data[quality]['main'][protocol].replace('http://', 'https://')
self.room_title = room_info['title']
except:
diff --git a/biliup/plugins/douyu.py b/biliup/plugins/douyu.py
index 28aea53..ab5cc72 100644
--- a/biliup/plugins/douyu.py
+++ b/biliup/plugins/douyu.py
@@ -11,12 +11,15 @@ from ..engine.download import DownloadBase
from ..plugins import logger, match1, random_user_agent
-@Plugin.download(regexp=r'(?:https?://)?(?:(?:www|m)\.)?douyu\.com')
+@Plugin.download(regexp=r'https?://(?:(?:www|m)\.)?douyu\.com')
class Douyu(DownloadBase):
def __init__(self, fname, url, suffix='flv'):
super().__init__(fname, url, suffix)
self.__room_id = match1(url, r'rid=(\d+)')
self.douyu_danmaku = config.get('douyu_danmaku', False)
+ self.douyu_disable_interactive_game = config.get('douyu_disable_interactive_game', False)
+ self.douyu_cdn = config.get('douyu_cdn', 'tct-h5')
+ self.douyu_rate = config.get('douyu_rate', 0)
async def acheck_stream(self, is_check=False):
if len(self.url.split("douyu.com/")) < 2:
@@ -44,7 +47,7 @@ class Douyu(DownloadBase):
if room_info['videoLoop'] != 0:
logger.debug(f"{self.plugin_msg}: 正在放录播")
return False
- if config.get('douyu_disable_interactive_game', False):
+ if self.douyu_disable_interactive_game:
gift_info = (
await client.get(f"https://www.douyu.com/api/interactive/web/v2/list?rid={self.__room_id}",
headers=self.fake_headers)
@@ -90,9 +93,8 @@ class Douyu(DownloadBase):
logger.exception(f"{self.plugin_msg}: 获取签名参数异常")
return False
- params['cdn'] = config.get('douyucdn', 'tct-h5')
- params['cdn'] = config.get('douyu_cdn', params['cdn'])
- params['rate'] = config.get('douyu_rate', 0)
+ params['cdn'] = self.douyu_cdn
+ params['rate'] = int(self.douyu_rate)
try:
live_data = await self.get_play_info(self.__room_id, params)
diff --git a/biliup/plugins/huya.py b/biliup/plugins/huya.py
index 4b688a8..b146171 100644
--- a/biliup/plugins/huya.py
+++ b/biliup/plugins/huya.py
@@ -14,7 +14,7 @@ from ..engine.download import DownloadBase
from ..plugins import logger, match1, random_user_agent
-@Plugin.download(regexp=r'(?:https?://)?(?:(?:www|m)\.)?huya\.com')
+@Plugin.download(regexp=r'https?://(?:(?:www|m)\.)?huya\.com')
class Huya(DownloadBase):
def __init__(self, fname, url, suffix='flv'):
super().__init__(fname, url, suffix)
@@ -22,12 +22,15 @@ class Huya(DownloadBase):
self.fake_headers['cookie'] = config.get('user', {}).get('huya_cookie', '')
self.__room_id = url.split('huya.com/')[1].split('?')[0]
self.huya_danmaku = config.get('huya_danmaku', False)
-
+ self.huya_max_ratio = config.get('huya_max_ratio', 0)
+ self.huya_cdn = config.get('huya_cdn', "").upper() # 不填写时使用主播的CDN优先级
+ self.huya_protocol = 'Hls' if config.get('huya_protocol') == 'Hls' else 'Flv'
+ self.huya_imgplus = config.get('huya_imgplus', True)
+ self.huya_cdn_fallback = config.get('huya_cdn_fallback', False)
+ self.huya_mobile_api = config.get('huya_mobile_api', False)
async def acheck_stream(self, is_check=False):
try:
- if self.fake_headers.get('cookie'):
- await self.verify_cookie()
if not self.__room_id.isdigit():
self.__room_id = _get_real_rid(self.url)
room_profile = await self.get_room_profile(use_api=True)
@@ -53,9 +56,9 @@ class Huya(DownloadBase):
if is_check:
return True
- huya_max_ratio = config.get('huya_max_ratio', 0)
- if huya_max_ratio:
+ if self.huya_max_ratio:
try:
+ self.huya_max_ratio = int(self.huya_max_ratio)
# 最大码率(不含hdr)
# max_ratio = html_info['data'][0]['gameLiveInfo']['bitRate']
max_ratio = room_profile['liveData']['bitRate']
@@ -64,7 +67,7 @@ class Huya(DownloadBase):
# 码率信息
ratio_items = [r.get('iBitRate', 0) if r.get('iBitRate', 0) != 0 else max_ratio for r in live_rate_info]
# 符合条件的码率
- ratio_in_items = [x for x in ratio_items if x <= huya_max_ratio]
+ ratio_in_items = [x for x in ratio_items if x <= self.huya_max_ratio]
# 录制码率
if ratio_in_items:
record_ratio = max(ratio_in_items)
@@ -76,43 +79,37 @@ class Huya(DownloadBase):
is_xingxiu = room_profile['liveData']['gid'] == 1663
- perf_cdn = config.get('huya_cdn', "").upper() # 不填写时使用主播的CDN优先级
- protocol = 'Hls' if config.get('huya_protocol') == 'Hls' else 'Flv'
- allow_imgplus = config.get('huya_imgplus', True)
- cdn_fallback = config.get('huya_cdn_fallback', False)
- use_api = config.get('huya_mobile_api', False)
-
try:
- stream_urls = await self.get_stream_urls(protocol, use_api, allow_imgplus, is_xingxiu)
+ stream_urls = await self.get_stream_urls(self.huya_protocol, self.huya_mobile_api, self.huya_imgplus, is_xingxiu)
except Exception as e:
logger.error(f"{self.plugin_msg}: 没有可用的链接 {e}")
return False
cdn_name = list(stream_urls.keys())
- if not perf_cdn or perf_cdn not in cdn_name:
- perf_cdn = cdn_name[0]
+ if not self.huya_cdn or self.huya_cdn not in cdn_name:
+ self.huya_cdn = cdn_name[0]
# 虎牙直播流只允许连接一次
- if cdn_fallback:
- _url = await self.acheck_url_healthy(stream_urls[perf_cdn])
+ if self.huya_cdn_fallback:
+ _url = await self.acheck_url_healthy(stream_urls[self.huya_cdn])
if _url is None:
logger.info(f"{self.plugin_msg}: cdn_fallback 顺序尝试 {cdn_name}")
for cdn in cdn_name:
logger.info(f"{self.plugin_msg}: cdn_fallback-{cdn}")
if (await self.acheck_url_healthy(stream_urls[cdn])) is None:
continue
- perf_cdn = cdn
- logger.info(f"{self.plugin_msg}: cdn_fallback 回退到 {perf_cdn}")
+ self.huya_cdn = cdn
+ logger.info(f"{self.plugin_msg}: cdn_fallback 回退到 {self.huya_cdn}")
break
else:
logger.error(f"{self.plugin_msg}: cdn_fallback 所有链接无法使用")
return False
- stream_urls = await self.get_stream_urls(protocol, use_api, allow_imgplus, is_xingxiu)
+ stream_urls = await self.get_stream_urls(self.huya_protocol, self.huya_mobile_api, self.huya_imgplus, is_xingxiu)
self.room_title = room_profile['liveData']['introduction']
- self.raw_stream_url = stream_urls[perf_cdn]
+ self.raw_stream_url = stream_urls[self.huya_cdn]
- if huya_max_ratio and record_ratio != max_ratio:
+ if self.huya_max_ratio and record_ratio != max_ratio:
self.raw_stream_url += f"&ratio={record_ratio}"
return True
diff --git a/biliup/plugins/kilakila.py b/biliup/plugins/kilakila.py
index f9d331c..28869da 100644
--- a/biliup/plugins/kilakila.py
+++ b/biliup/plugins/kilakila.py
@@ -7,10 +7,11 @@ from ..engine.decorators import Plugin
from ..engine.download import DownloadBase
-@Plugin.download(regexp=r'(?:https?://)?(live\.kilakila\.cn|www\.hongdoufm\.com)')
+@Plugin.download(regexp=r'https?://(live\.kilakila\.cn|www\.hongdoufm\.com)')
class Kilakila(DownloadBase):
def __init__(self, fname, url, suffix='flv'):
self._room_id: str = match1(url, r'(\d+)')
+ self.kila_protocol = config.get('kila_protocol', 'hls')
super().__init__(fname, url, suffix)
async def acheck_stream(self, is_check=False):
@@ -55,7 +56,7 @@ class Kilakila(DownloadBase):
# self.background_pic_url = r['b']['defaultBackgroundPicUrl']
self.room_title = r['b']['title']
self.live_cover_url = r['b']['backPic']
- if config.get('kila_protocol') == 'flv':
+ if self.kila_protocol == 'flv':
self.raw_stream_url = r['b']['flvPlayUrl']
else:
self.raw_stream_url = r['b']['hlsPlayUrl']
diff --git a/biliup/plugins/twitch.py b/biliup/plugins/twitch.py
index 5f3e993..3a7deb7 100644
--- a/biliup/plugins/twitch.py
+++ b/biliup/plugins/twitch.py
@@ -16,7 +16,7 @@ from . import logger
from ..engine.decorators import Plugin
from ..engine.download import DownloadBase, BatchCheck
-VALID_URL_BASE = r'(?:https?://)?(?:(?:www|go|m)\.)?twitch\.tv/(?P[0-9_a-zA-Z]+)'
+VALID_URL_BASE = r'https?://(?:(?:www|go|m)\.)?twitch\.tv/(?P[0-9_a-zA-Z]+)'
VALID_URL_VIDEOS = r'https?://(?:(?:www|go|m)\.)?twitch\.tv/(?P[^/]+)/(?:videos|profile|clips)'
_CLIENT_ID = 'kimne78kx3ncx6brgo4mv6wki5h1ko'