From fe5b9c986dc7ff7c0f43fdccc0376f0d0d8d4fac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A5=B6=E7=B2=89?= <53338804+KNaiFen@users.noreply.github.com> Date: Tue, 6 Jun 2023 10:31:56 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9ENICO=E5=BD=95=E5=88=B6?= =?UTF-8?q?=EF=BC=8C=E6=96=B0=E5=A2=9EYoutube=E7=9A=84StreamLink=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=E6=A8=A1=E5=BC=8F=EF=BC=8C=E9=85=8D=E7=BD=AE=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E6=96=87=E6=A1=88=E4=BF=AE=E6=AD=A3=20(#460)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 增加Twitch的去广告开关(解决广告分段问题) 修改当请求流错误时等待delay的值的合理性,写死为30秒(防止有人等待的太久) 增加、修正toml,yaml文件的配置和注释 * 新增NICO的录制,登录信息选项在配置文件的user中 新增Youtube的StreamLink下载模式,由配置文件里use_new_ytb_downloader,默认关闭 配置文件文案修正 --------- Co-authored-by: KNaiFen --- biliup/plugins/Danmaku/__init__.py | 6 +- biliup/plugins/Danmaku/paramgen/__init__.py | 1 + biliup/plugins/Danmaku/paramgen/arcparam.py | 44 +++++ biliup/plugins/Danmaku/paramgen/enc.py | 24 +++ biliup/plugins/Danmaku/paramgen/liveparam.py | 84 +++++++++ biliup/plugins/Danmaku/youtube.py | 137 ++++++++++++++ biliup/plugins/nico.py | 68 +++++++ biliup/plugins/youtube.py | 187 ++++++++++++------- public/config.toml | 4 + public/config.yaml | 3 + 10 files changed, 489 insertions(+), 69 deletions(-) create mode 100644 biliup/plugins/Danmaku/paramgen/__init__.py create mode 100644 biliup/plugins/Danmaku/paramgen/arcparam.py create mode 100644 biliup/plugins/Danmaku/paramgen/enc.py create mode 100644 biliup/plugins/Danmaku/paramgen/liveparam.py create mode 100644 biliup/plugins/Danmaku/youtube.py create mode 100644 biliup/plugins/nico.py diff --git a/biliup/plugins/Danmaku/__init__.py b/biliup/plugins/Danmaku/__init__.py index 616dadf..0116364 100644 --- a/biliup/plugins/Danmaku/__init__.py +++ b/biliup/plugins/Danmaku/__init__.py @@ -41,7 +41,11 @@ class DanmakuClient: self.__url = url else: self.__url = 'http://' + url - for u, s in {'douyu.com': Douyu, 'huya.com': Huya, 'live.bilibili.com': Bilibili, 'twitch.tv': Twitch}.items(): + for u, s in {'douyu.com': Douyu, + 'huya.com': Huya, + 'live.bilibili.com': Bilibili, + 'twitch.tv': Twitch + }.items(): if re.match(r'^(?:http[s]?://)?.*?%s/(.+?)$' % u, url): self.__site = s self.__u = u diff --git a/biliup/plugins/Danmaku/paramgen/__init__.py b/biliup/plugins/Danmaku/paramgen/__init__.py new file mode 100644 index 0000000..0aba17d --- /dev/null +++ b/biliup/plugins/Danmaku/paramgen/__init__.py @@ -0,0 +1 @@ +import os \ No newline at end of file diff --git a/biliup/plugins/Danmaku/paramgen/arcparam.py b/biliup/plugins/Danmaku/paramgen/arcparam.py new file mode 100644 index 0000000..76240ee --- /dev/null +++ b/biliup/plugins/Danmaku/paramgen/arcparam.py @@ -0,0 +1,44 @@ +from . import enc +from base64 import urlsafe_b64encode as b64enc +from urllib.parse import quote + + +def _header(video_id, channel_id) -> str: + S1_3 = enc.rs(1, video_id) + S1_5 = enc.rs(1, channel_id) + enc.rs(2, video_id) + S1 = enc.rs(3, S1_3) + enc.rs(5, S1_5) + S3 = enc.rs(48687757, enc.rs(1, video_id)) + header_replay = enc.rs(1, S1) + enc.rs(3, S3) + enc.nm(4, 1) + return b64enc(header_replay) + + +def _build(video_id, seektime, topchat_only, channel_id) -> str: + chattype = 4 if topchat_only else 1 + if seektime < 0: + seektime = 0 + timestamp = int(seektime * 1000000) + header = enc.rs(3, _header(video_id, channel_id)) + timestamp = enc.nm(5, timestamp) + s6 = enc.nm(6, 0) + s7 = enc.nm(7, 0) + s8 = enc.nm(8, 0) + s9 = enc.nm(9, 4) + s10 = enc.rs(10, enc.nm(4, 0)) + chattype = enc.rs(14, enc.nm(1, 4)) + s15 = enc.nm(15, 0) + entity = b"".join((header, timestamp, s6, s7, s8, s9, s10, chattype, s15)) + continuation = enc.rs(156074452, entity) + return quote(b64enc(continuation).decode()) + + +def getparam(video_id, seektime=0, topchat_only=False, channel_id="") -> str: + """ + Parameter + --------- + seektime : int + unit:seconds + start position of fetching chat data. + topchat_only : bool + if True, fetch only 'top chat' + """ + return _build(video_id, seektime, topchat_only, channel_id) diff --git a/biliup/plugins/Danmaku/paramgen/enc.py b/biliup/plugins/Danmaku/paramgen/enc.py new file mode 100644 index 0000000..8e3b8fe --- /dev/null +++ b/biliup/plugins/Danmaku/paramgen/enc.py @@ -0,0 +1,24 @@ +def vn(val): + if val < 0: + raise ValueError + buf = b"" + while val >> 7: + m = val & 0xFF | 0x80 + buf += m.to_bytes(1, "big") + val >>= 7 + buf += val.to_bytes(1, "big") + return buf + + +def tp(a, b, ary): + return vn((b << 3) | a) + ary + + +def rs(a, ary): + if isinstance(ary, str): + ary = ary.encode() + return tp(2, a, vn(len(ary)) + ary) + + +def nm(a, ary): + return tp(0, a, vn(ary)) diff --git a/biliup/plugins/Danmaku/paramgen/liveparam.py b/biliup/plugins/Danmaku/paramgen/liveparam.py new file mode 100644 index 0000000..5e15564 --- /dev/null +++ b/biliup/plugins/Danmaku/paramgen/liveparam.py @@ -0,0 +1,84 @@ +import random +import time +from . import enc +from base64 import urlsafe_b64encode as b64enc +from urllib.parse import quote + + +def _header(video_id, channel_id) -> str: + S1_3 = enc.rs(1, video_id) + S1_5 = enc.rs(1, channel_id) + enc.rs(2, video_id) + S1 = enc.rs(3, S1_3) + enc.rs(5, S1_5) + S3 = enc.rs(48687757, enc.rs(1, video_id)) + header_replay = enc.rs(1, S1) + enc.rs(3, S3) + enc.nm(4, 1) + return b64enc(header_replay) + + +def _build(video_id, channel_id, ts1, ts2, ts3, ts4, ts5, topchat_only) -> str: + chattype = 4 if topchat_only else 1 + + b1 = enc.nm(1, 0) + b2 = enc.nm(2, 0) + b3 = enc.nm(3, 0) + b4 = enc.nm(4, 0) + b7 = enc.rs(7, "") + b8 = enc.nm(8, 0) + b9 = enc.rs(9, "") + timestamp2 = enc.nm(10, ts2) + b11 = enc.nm(11, 3) + b15 = enc.nm(15, 0) + + header = enc.rs(3, _header(video_id, channel_id)) + timestamp1 = enc.nm(5, ts1) + s6 = enc.nm(6, 0) + s7 = enc.nm(7, 0) + s8 = enc.nm(8, 1) + body = enc.rs(9, b"".join((b1, b2, b3, b4, b7, b8, b9, timestamp2, b11, b15))) + timestamp3 = enc.nm(10, ts3) + timestamp4 = enc.nm(11, ts4) + s13 = enc.nm(13, chattype) + chattype = enc.rs(16, enc.nm(1, chattype)) + s17 = enc.nm(17, 0) + str19 = enc.rs(19, enc.nm(1, 0)) + timestamp5 = enc.nm(20, ts5) + entity = b"".join( + ( + header, + timestamp1, + s6, + s7, + s8, + body, + timestamp3, + timestamp4, + s13, + chattype, + s17, + str19, + timestamp5, + ) + ) + continuation = enc.rs(119693434, entity) + return quote(b64enc(continuation).decode()) + + +def _times(past_sec): + n = int(time.time()) + _ts1 = n - random.uniform(0, 1 * 3) + _ts2 = n - random.uniform(0.01, 0.99) + _ts3 = n - past_sec + random.uniform(0, 1) + _ts4 = n - random.uniform(10 * 60, 60 * 60) + _ts5 = n - random.uniform(0.01, 0.99) + return list(map(lambda x: int(x * 1000000), [_ts1, _ts2, _ts3, _ts4, _ts5])) + + +def getparam(video_id, channel_id, past_sec=0, topchat_only=False) -> str: + """ + Parameter + --------- + past_sec : int + seconds to load past chat data + topchat_only : bool + if True, fetch only 'top chat' + """ + return _build(video_id, channel_id, *_times(past_sec), topchat_only) diff --git a/biliup/plugins/Danmaku/youtube.py b/biliup/plugins/Danmaku/youtube.py new file mode 100644 index 0000000..0942c14 --- /dev/null +++ b/biliup/plugins/Danmaku/youtube.py @@ -0,0 +1,137 @@ +import json, re, select, random, traceback, urllib, datetime, base64 +import asyncio, aiohttp + +# The core codes for YouTube support are basically from taizan-hokuto/pytchat + +headers = { + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36", +} + + +class Youtube: + q = None + url = "" + vid = "" + ctn = "" + client = None + stop = False + + @classmethod + async def run(cls, url, q, client, **kargs): + from .paramgen import liveparam + + cls.q = q + cls.url = url + cls.client = client + cls.stop = False + cls.key = "eW91dHViZWkvdjEvbGl2ZV9jaGF0L2dldF9saXZlX2NoYXQ/a2V5PUFJemFTeUFPX0ZKMlNscVU4UTRTVEVITEdDaWx3X1k5XzExcWNXOA==" + await cls.get_url() + while cls.stop == False: + try: + await cls.get_room_info() + cls.ctn = liveparam.getparam(cls.vid, cls.cid, 1) + await cls.get_chat() + except: + traceback.print_exc() + await asyncio.sleep(1) + + @classmethod + async def stop(cls): + cls.stop = True + + @classmethod + async def get_url(cls): + a = re.search(r"youtube.com/channel/([^/?]+)", cls.url) + try: + cid = a.group(1) + cls.cid = cid + cls.url = f"https://www.youtube.com/channel/{cid}/videos" + except: + a = re.search(r"youtube.com/watch\?v=([^/?]+)", cls.url) + async with cls.client.request( + "get", f"https://www.youtube.com/embed/{a.group(1)}" + ) as resp: + b = re.search(r'\\"channelId\\":\\"(.{24})\\"', await resp.text()) + cls.cid = b.group(1) + cls.url = f"https://www.youtube.com/channel/{cls.cid}/videos" + + @classmethod + async def get_room_info(cls): + async with cls.client.request("get", cls.url) as resp: + t = re.search( + r'"gridVideoRenderer"((.(?!"gridVideoRenderer"))(?!"style":"UPCOMING"))+"label":"(LIVE|LIVE NOW|PREMIERING NOW)"([\s\S](?!"style":"UPCOMING"))+?("gridVideoRenderer"|)', + await resp.text(), + ).group(0) + cls.vid = re.search(r'"gridVideoRenderer".+?"videoId":"(.+?)"', t).group(1) + # print(cls.vid) + + @classmethod + async def get_chat_single(cls): + msgs = [] + data = { + "context": { + "client": { + "visitorData": "", + "userAgent": headers["user-agent"], + "clientName": "WEB", + "clientVersion": "".join( + ( + "2.", + (datetime.datetime.today() - datetime.timedelta(days=1)).strftime( + "%Y%m%d" + ), + ".01.00", + ) + ), + }, + }, + "continuation": cls.ctn, + } + u = f'https://www.youtube.com/{base64.b64decode(cls.key).decode("utf-8")}' + async with cls.client.request("post", u, headers=headers, json=data) as resp: + # print(await resp.text()) + j = await resp.json() + j = j["continuationContents"] + cont = j["liveChatContinuation"]["continuations"][0] + if cont is None: + raise Exception("No Continuation") + metadata = ( + cont.get("invalidationContinuationData") + or cont.get("timedContinuationData") + or cont.get("reloadContinuationData") + or cont.get("liveChatReplayContinuationData") + ) + cls.ctn = metadata["continuation"] + # print(j['liveChatContinuation'].get('actions')) + for action in j["liveChatContinuation"].get("actions", []): + try: + renderer = action["addChatItemAction"]["item"]["liveChatTextMessageRenderer"] + msg = {} + msg["name"] = renderer["authorName"]["simpleText"] + message = "" + runs = renderer["message"].get("runs") + for r in runs: + if r.get("emoji"): + message += r["emoji"].get("shortcuts", [""])[0] + else: + message += r.get("text", "") + msg["content"] = message + msg["msg_type"] = "danmaku" + msg["color"] = "16777215" + msgs.append(msg) + except: + pass + + return msgs + + @classmethod + async def get_chat(cls): + while cls.stop == False: + ms = await cls.get_chat_single() + if len(ms) != 0: + interval = 1 / len(ms) + else: + await asyncio.sleep(1) + for m in ms: + await cls.q.put(m) + await asyncio.sleep(interval) diff --git a/biliup/plugins/nico.py b/biliup/plugins/nico.py new file mode 100644 index 0000000..554d4d6 --- /dev/null +++ b/biliup/plugins/nico.py @@ -0,0 +1,68 @@ +import random +import string +import json +import subprocess +import time + +import requests +from . import logger +import requests +import json +from biliup.config import config +from ..engine.decorators import Plugin +from ..engine.download import DownloadBase +from bs4 import BeautifulSoup +from ..plugins import logger + + +@Plugin.download(regexp=r'(?:https?://)?(?:(?:www|m|live)\.)?nicovideo\.jp') +class Nico(DownloadBase): + def __init__(self, fname, url, suffix='flv'): + super().__init__(fname, url, suffix) + + def check_stream(self): + try: + response = requests.get(self.url) + soup = BeautifulSoup(response.text, 'html.parser') + script_tag = soup.find('script', {'type': 'application/ld+json'}) + if script_tag: + data = script_tag.string + json_sc = json.loads(data) + self.room_title = json_sc['name'] + except: + logger.info("获取标题失败") + port = random.randint(1025, 65535) + stream_shell = [ + "streamlink", + "--player-external-http", # 为外部程序提供流媒体数据 + "--player-external-http-port", str(port), # 对外部输出流的端口 + self.url, "best" # 流链接 + ] + if config.get('user', {}).get('niconico-email') is not None: + niconico_email = "--niconico-email " + config.get('user', {}).get('niconico-email') + stream_shell.insert(1, niconico_email) + if config.get('user', {}).get('niconico-password') is not None: + niconico_password = "--niconico-password " + config.get('user', {}).get('niconico-password') + stream_shell.insert(1, niconico_password) + if config.get('user', {}).get('niconico-user-session') is not None: + niconico_user_session = "--niconico-user-session " + config.get('user', {}).get('niconico-user-session') + stream_shell.insert(1, niconico_user_session) + if config.get('user', {}).get('niconico-purge-credentials') is not None: + niconico_purge_credentials = "--niconico-purge-credentials " + config.get('user', {}).get('niconico-purge-credentials') + stream_shell.insert(1, niconico_purge_credentials) + self.proc = subprocess.Popen(stream_shell) + self.raw_stream_url = f"http://localhost:{port}" + i = 0 + while i < 5: + if not (self.proc.poll() is None): + return False + time.sleep(1) + i += 1 + return True + + def close(self): + try: + if self.proc is not None: + self.proc.terminate() + except: + logger.exception(f'terminate {self.fname} failed') diff --git a/biliup/plugins/youtube.py b/biliup/plugins/youtube.py index e703dc3..0021cf0 100644 --- a/biliup/plugins/youtube.py +++ b/biliup/plugins/youtube.py @@ -1,4 +1,11 @@ +import asyncio +import random +import subprocess +import threading +from urllib.parse import urlparse + import yt_dlp +import streamlink import os import time from PIL import Image @@ -6,8 +13,9 @@ from yt_dlp.utils import MaxDownloadsReached from yt_dlp.utils import DateRange from biliup.config import config +from biliup.plugins.Danmaku import DanmakuClient from ..engine.decorators import Plugin -from ..engine.download import DownloadBase +from ..engine.download import DownloadBase, get_valid_filename, stream_gears_download from . import logger VALID_URL_BASE = r'https?://(?:(?:www|m)\.)?youtube\.com/(?P.*?)\??(.*?)' @@ -16,6 +24,8 @@ VALID_URL_BASE = r'https?://(?:(?:www|m)\.)?youtube\.com/(?P.*?)\??(.*?)' class Youtube(DownloadBase): def __init__(self, fname, url, suffix='webm'): DownloadBase.__init__(self, fname, url, suffix=suffix) + self.use_new_ytb_downloader = config.get('use_new_ytb_downloader', False) + self.ytb_danmaku = config.get('ytb_danmaku', False) self.cookiejarFile = config.get('user', {}).get('youtube_cookie') self.vcodec = config.get('youtube_prefer_vcodec','av01|vp9|avc') self.acodec = config.get('youtube_prefer_acodec','opus|mp4a') @@ -26,73 +36,114 @@ class Youtube(DownloadBase): self.use_youtube_cover = config.get('use_youtube_cover', False) def check_stream(self): - with yt_dlp.YoutubeDL({'download_archive': 'archive.txt', 'ignoreerrors': True, 'extract_flat': True, - 'cookiefile': self.cookiejarFile}) as ydl: - info = ydl.extract_info(self.url, download=False, process=False) - if info is None: - logger.warning(self.cookiejarFile) - logger.warning(self.url) - return False - if '_type' in info: - # /live 形式链接取标题 - if info['_type'] in 'url' and info['webpage_url_basename'] in 'live': - live = ydl.extract_info(info['url'], download=False, process=False) - self.room_title = live['title'] - # Playlist 暂时返回列表名 - if info['_type'] in 'playlist': - self.room_title = info['title'] - else: - # 视频取标题 - self.room_title = info['title'] - if info.get('entries') is None: - if ydl.in_download_archive(info): + if self.use_new_ytb_downloader and self.downloader == 'ffmpeg': + _fname, self.room_title = self.get_stream_info(self.url) + port = random.randint(1025, 65535) + stream_shell = [ + "streamlink", + "--player-external-http", # 为外部程序提供流媒体数据 + "--player-external-http-port", str(port), # 对外部输出流的端口 + self.url, "best" # 流链接 + ] + self.proc = subprocess.Popen(stream_shell) + self.raw_stream_url = f"http://localhost:{port}" + i = 0 + while i < 5: + if not (self.proc.poll() is None): + return + time.sleep(1) + i += 1 + return True + else: + with yt_dlp.YoutubeDL({'download_archive': 'archive.txt', 'ignoreerrors': True, 'extract_flat': True, + 'cookiefile': self.cookiejarFile}) as ydl: + info = ydl.extract_info(self.url, download=False, process=False) + if info is None: + logger.warning(self.cookiejarFile) + logger.warning(self.url) return False - return True - for entry in info['entries']: - # 取 Playlist 内视频标题 - self.room_title = entry['title'] - if ydl.in_download_archive(entry): - continue - # ydl.record_download_archive() - return True + if '_type' in info: + # /live 形式链接取标题 + if info['_type'] in 'url' and info['webpage_url_basename'] in 'live': + live = ydl.extract_info(info['url'], download=False, process=False) + self.room_title = live['title'] + # Playlist 暂时返回列表名 + if info['_type'] in 'playlist': + self.room_title = info['title'] + else: + # 视频取标题 + self.room_title = info['title'] + if info.get('entries') is None: + if ydl.in_download_archive(info): + return False + return True + for entry in info['entries']: + # 取 Playlist 内视频标题 + self.room_title = entry['title'] + if ydl.in_download_archive(entry): + continue + # ydl.record_download_archive() + return True def download(self, filename): - try: - ydl_opts = { - 'outtmpl': filename+ '.%(ext)s', - 'format': f"bestvideo[vcodec~='^({self.vcodec})'][height<={self.resolution}][filesize<{self.filesize}]+bestaudio[acodec~='^({self.acodec})']/best[height<={self.resolution}]/best", - 'cookiefile': self.cookiejarFile, - # 'proxy': proxyUrl, - 'ignoreerrors': True, - 'max_downloads': 1, - 'daterange' : DateRange(self.afterdate, self.beforedate), - 'break_on_reject': True, - 'download_archive': 'archive.txt', - } - if self.use_youtube_cover is True: - ydl_opts['writethumbnail'] = True - with yt_dlp.YoutubeDL(ydl_opts) as ydl: - ydl.download([self.url]) - except MaxDownloadsReached: - save_dir = f'cover/youtube/' - webp_cover_path = f'{filename}.webp' - jpg_cover_path = f'{filename}.jpg' - if os.path.exists(webp_cover_path): - with Image.open(webp_cover_path) as img: - img = img.convert('RGB') - if not os.path.exists(save_dir): - os.makedirs(save_dir) - img.save(f'{save_dir}{filename}.jpg', format='JPEG') - os.remove(webp_cover_path) - self.live_cover_path = f'{save_dir}{filename}.jpg' - elif os.path.exists(jpg_cover_path): - os.rename(jpg_cover_path, f'{save_dir}{filename}.jpg') - self.live_cover_path = f'{save_dir}{filename}.jpg' - return False - except yt_dlp.utils.RejectedVideoReached: - logger.info("已下载完毕指定日期内的视频,结束本次任务") - return False - except yt_dlp.utils.DownloadError: - logger.exception(self.fname) - return 1 - return 0 + if self.use_new_ytb_downloader and self.downloader == 'ffmpeg': + if self.filename_prefix: # 判断是否存在自定义录播命名设置 + filename = (self.filename_prefix.format(streamer=self.fname, title=self.room_title).encode( + 'unicode-escape').decode()).encode().decode("unicode-escape") + else: + filename = f'{self.fname}%Y-%m-%dT%H_%M_%S' + filename = get_valid_filename(filename) + fmtname = time.strftime(filename.encode("unicode-escape").decode()).encode().decode("unicode-escape") + loop = asyncio.new_event_loop() + threading.Thread(target=loop.run_until_complete, args=(self.danmaku_download_start(fmtname),)).start() + self.ffmpeg_download(fmtname) + else: + try: + ydl_opts = { + 'outtmpl': filename+ '.%(ext)s', + 'format': f"bestvideo[vcodec~='^({self.vcodec})'][height<={self.resolution}][filesize<{self.filesize}]+bestaudio[acodec~='^({self.acodec})']/best[height<={self.resolution}]/best", + 'cookiefile': self.cookiejarFile, + # 'proxy': proxyUrl, + 'ignoreerrors': True, + 'max_downloads': 1, + 'daterange' : DateRange(self.afterdate, self.beforedate), + 'break_on_reject': True, + 'download_archive': 'archive.txt', + } + if self.use_youtube_cover is True: + ydl_opts['writethumbnail'] = True + with yt_dlp.YoutubeDL(ydl_opts) as ydl: + ydl.download([self.url]) + except MaxDownloadsReached: + save_dir = f'cover/youtube/' + webp_cover_path = f'{filename}.webp' + jpg_cover_path = f'{filename}.jpg' + if os.path.exists(webp_cover_path): + with Image.open(webp_cover_path) as img: + img = img.convert('RGB') + if not os.path.exists(save_dir): + os.makedirs(save_dir) + img.save(f'{save_dir}{filename}.jpg', format='JPEG') + os.remove(webp_cover_path) + self.live_cover_path = f'{save_dir}{filename}.jpg' + elif os.path.exists(jpg_cover_path): + os.rename(jpg_cover_path, f'{save_dir}{filename}.jpg') + self.live_cover_path = f'{save_dir}{filename}.jpg' + return False + except yt_dlp.utils.RejectedVideoReached: + logger.info("已下载完毕指定日期内的视频,结束本次任务") + return False + except yt_dlp.utils.DownloadError: + logger.exception(self.fname) + return 1 + return 0 + + def get_stream_info(self, url): + session = streamlink.Streamlink() + # Streamlink 插件系统的内部运行机制,通过 URL 匹配到 YouTube 插件 + plugin = session.resolve_url(url)[1] + streams = plugin(session=session, url=url) + streams._get_streams() + author = streams.get_author() + title = streams.get_title() + return author, title \ No newline at end of file diff --git a/public/config.toml b/public/config.toml index 132a00b..1f81e3e 100644 --- a/public/config.toml +++ b/public/config.toml @@ -118,6 +118,10 @@ check_sourcecode = 15 #youtube_after_date = '20220201' ### 仅下载该日期之前的视频(可与上面的youtube_after_date配合使用,构成指定下载范围区间) #youtube_before_date = '20230501' +### YouTube的Streamlink下载器模式,开启之后会调用Streamlink获取直播流并且使用FFMPEG下载,默认关闭【仅当下载器是FFMPEG时才有效】 +### 开启之后上面关于YOUTUBE的选项全部无效【我也不知道这个功能有什么屌用】 +#use_new_ytb_downloader = false + #------Twitch------# ### 录制Twitch弹幕,默认关闭【只有下载器为FFMPEG时才有效】 diff --git a/public/config.yaml b/public/config.yaml index c001eb6..1ccb5f7 100644 --- a/public/config.yaml +++ b/public/config.yaml @@ -116,6 +116,9 @@ check_sourcecode: 15 #youtube_after_date: '20220201' ### 仅下载该日期之前的视频(可与上面的youtube_after_date配合使用,构成指定下载范围区间) #youtube_before_date: '20230501' +### YouTube的Streamlink下载器模式,开启之后会调用Streamlink获取直播流并且使用FFMPEG下载,默认关闭【仅当下载器是FFMPEG时才有效】 +### 开启之后上面关于YOUTUBE的选项全部无效【我也不知道这个功能有什么屌用】 +use_new_ytb_downloader: false #------Twitch------# ### 录制Twitch弹幕,默认关闭【只有下载器为FFMPEG时才有效】