Skip to content

Commit

Permalink
add multi key support, add new option stream_retry
Browse files Browse the repository at this point in the history
  • Loading branch information
HFrost0 committed Feb 5, 2023
1 parent e48adc0 commit 1b34fc6
Show file tree
Hide file tree
Showing 15 changed files with 119 additions and 101 deletions.
41 changes: 26 additions & 15 deletions bilix/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,29 +40,29 @@ def print_help():
console.print(f"\n[bold]bilix {__version__}", justify="center")
console.print("⚡️快如闪电的bilibili下载工具,基于Python现代Async特性,高速批量下载整部动漫,电视剧,up投稿等\n",
justify="center")
console.print("使用方法: bilix [cyan]<method> <key> [OPTIONS][/cyan] ", justify="left")
console.print("使用方法: bilix [cyan]<method> <key1, key2...> [OPTIONS][/cyan] ", justify="left")
table = Table.grid(padding=1, pad_edge=False)
table.add_column("Parameter", no_wrap=True, justify="left", style="bold")
table.add_column("Description")

table.add_row(
"[cyan]<method>",
'get_series 或 s:获取整个系列的视频(包括多p投稿,动漫,电视剧,电影,纪录片),也可以下载单个视频\n'
'get_video 或 v:获取特定的单个视频,在用户不希望下载系列其他视频的时候可以使用\n'
'get_up 或 up:获取某个up的所有投稿视频,支持数量选择,关键词搜索,排序\n'
'get_cate 或 cate:获取分区视频,支持数量选择,关键词搜索,排序\n'
'get_favour 或 fav:获取收藏夹内视频,支持数量选择,关键词搜索\n'
'get_series 或 s: 获取整个系列的视频(包括多p投稿,动漫,电视剧,电影,纪录片),也可以下载单个视频\n'
'get_video 或 v: 获取特定的单个视频,在用户不希望下载系列其他视频的时候可以使用\n'
'get_up 或 up: 获取某个up的所有投稿视频,支持数量选择,关键词搜索,排序\n'
'get_cate 或 cate: 获取分区视频,支持数量选择,关键词搜索,排序\n'
'get_favour 或 fav: 获取收藏夹内视频,支持数量选择,关键词搜索\n'
'get_collect 或 col:获取合集或视频列表内视频\n'
'info:打印url所属资源的详细信息(例如点赞数,画质,编码格式等)'
'info: 打印url所属资源的详细信息(例如点赞数,画质,编码格式等)'
)
table.add_row(
"[cyan]<key>",
'如果使用get_video或get_series,在此填写视频的url\n'
'如果使用get_up,则在该位置填写b站用户空间页url或用户id\n'
'如果使用get_cate,则在该位置填写分区名称\n'
'如果使用get_favour,则在该位置填写收藏夹页url或收藏夹id\n'
'如果使用get_collect,则在该位置填写合集或者视频列表详情页url\n'
'如果使用info,则在该位置填写任意资源url'
"[cyan]<key>[/cyan]",
'如使用get_video/get_series,填写视频的url\n'
'如使用get_up,填写b站用户空间页url或用户id\n'
'如使用get_cate,填写分区名称\n'
'如使用get_favour,填写收藏夹页url或收藏夹id\n'
'如使用get_collect,填写合集或者视频列表详情页url\n'
'如使用info,填写任意资源url'
)
console.print(table)
# console.rule("OPTIONS参数")
Expand Down Expand Up @@ -151,6 +151,10 @@ def print_help():
"-sl --speed-limit", '[dark_cyan]str',
'最大下载速度,默认无限制。例如:-sl 1.5MB (experimental)',
)
table.add_row(
"-sr --stream-retry", '[dark_cyan]int',
'下载过程中发生网络错误后最大重试数,默认5',
)
table.add_row("-h --help", '', "帮助信息")
table.add_row("-v --version", '', "版本信息")
table.add_row("--debug", '', "显示debug信息")
Expand Down Expand Up @@ -181,7 +185,7 @@ def convert(self, value, param, ctx):

@click.command(add_help_option=False)
@click.argument("method", type=str)
@click.argument("key", type=str)
@click.argument("keys", type=str, nargs=-1, required=True)
@click.option(
"--dir",
"videos_dir",
Expand Down Expand Up @@ -289,6 +293,13 @@ def convert(self, value, param, ctx):
type=BasedSpeedLimit(),
default=None,
)
@click.option(
'--stream-retry',
'-sr',
'stream_retry',
type=int,
default=5
)
@click.option(
'-h',
"--help",
Expand Down
3 changes: 2 additions & 1 deletion bilix/download/base_downloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class BaseDownloader:
DELAY_SLOPE: float = 0.1

def __init__(self, client: httpx.AsyncClient = None, videos_dir='videos', speed_limit: Union[float, int] = None,
progress: BaseProgress = None):
stream_retry=5, progress: BaseProgress = None):
"""
:param client: client used for http request
Expand All @@ -34,6 +34,7 @@ def __init__(self, client: httpx.AsyncClient = None, videos_dir='videos', speed_
else:
self.progress = progress
progress.holder = self
self.stream_retry = stream_retry
# active stream number
self._stream_num = 0

Expand Down
30 changes: 14 additions & 16 deletions bilix/download/base_downloader_m3u8.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@
from Crypto.Cipher import AES
from m3u8 import Segment

from bilix.handle import Handler, HandleMethodError
from bilix.handle import Handler
from bilix.download.base_downloader import BaseDownloader
from bilix.log import logger
from bilix.utils import req_retry, merge_files


class BaseDownloaderM3u8(BaseDownloader):
def __init__(self, client: httpx.AsyncClient = None, videos_dir="videos", video_concurrency=3, part_concurrency=10,
speed_limit: Union[float, int] = None, progress=None):
stream_retry=5, speed_limit: Union[float, int] = None, progress=None):
"""
Base async m3u8 Downloader
Expand All @@ -27,7 +27,8 @@ def __init__(self, client: httpx.AsyncClient = None, videos_dir="videos", video_
:param speed_limit:
:param progress:
"""
super(BaseDownloaderM3u8, self).__init__(client, videos_dir, speed_limit=speed_limit, progress=progress)
super(BaseDownloaderM3u8, self).__init__(
client, videos_dir, stream_retry=stream_retry, speed_limit=speed_limit, progress=progress)
self.v_sema = asyncio.Semaphore(video_concurrency)
self.part_con = part_concurrency
self.decrypt_cache = {}
Expand All @@ -48,14 +49,13 @@ async def get_key():
cipher = self.decrypt_cache[uri]
return cipher.decrypt(content)

async def get_m3u8_video(self, m3u8_url: str, name: str, hierarchy: str = '', retry: int = 5) -> str:
async def get_m3u8_video(self, m3u8_url: str, name: str, hierarchy: str = '') -> str:
"""
download
:param m3u8_url:
:param name:
:param hierarchy:
:param retry:
:return: downloaded file path
"""
base_path = f"{self.videos_dir}/{hierarchy}" if hierarchy else self.videos_dir
Expand All @@ -79,7 +79,7 @@ async def get_m3u8_video(self, m3u8_url: str, name: str, hierarchy: str = '', re
# https://stackoverflow.com/questions/50628791/decrypt-m3u8-playlist-encrypted-with-aes-128-without-iv
if seg.key and seg.key.iv is None:
seg.custom_parser_values['iv'] = idx.to_bytes(16, 'big')
cors.append(self._get_ts(seg, f"{name}-{idx}.ts", task_id, p_sema, hierarchy, retry=retry))
cors.append(self._get_ts(seg, f"{name}-{idx}.ts", task_id, p_sema, hierarchy))
await self.progress.update(task_id, total=0, total_time=total_time)
file_list = await asyncio.gather(*cors)
self.v_sema.release()
Expand All @@ -100,8 +100,7 @@ async def _update_task_total(self, task_id, time_part: float, update_size: int):
predicted_total = confirmed_b / confirmed_t * task.fields['total_time']
await self.progress.update(task_id, total=predicted_total, confirmed_t=confirmed_t, confirmed_b=confirmed_b)

async def _get_ts(self, seg: Segment, name, task_id, p_sema: asyncio.Semaphore, hierarchy: str = '',
retry: int = 5) -> str:
async def _get_ts(self, seg: Segment, name, task_id, p_sema: asyncio.Semaphore, hierarchy: str = '') -> str:
ts_url = seg.absolute_uri
base_path = f"{self.videos_dir}/{hierarchy}" if hierarchy else self.videos_dir
file_path = f"{base_path}/{name}"
Expand All @@ -113,7 +112,7 @@ async def _get_ts(self, seg: Segment, name, task_id, p_sema: asyncio.Semaphore,

async with p_sema:
content = None
for times in range(1 + retry):
for times in range(1 + self.stream_retry):
content = bytearray()
try:
async with self.client.stream("GET", ts_url,
Expand Down Expand Up @@ -142,15 +141,14 @@ async def _get_ts(self, seg: Segment, name, task_id, p_sema: asyncio.Semaphore,

@Handler.register(name="m3u8")
def handle(kwargs):
key = kwargs['key']
if re.fullmatch(r"http.+m3u8(\?.*)?", key):
method = kwargs['method']
if method == 'm3u8' or method == 'get_m3u8':
videos_dir = kwargs['videos_dir']
part_concurrency = kwargs['part_concurrency']
speed_limit = kwargs['speed_limit']
method = kwargs['method']
d = BaseDownloaderM3u8(videos_dir=videos_dir, part_concurrency=part_concurrency,
speed_limit=speed_limit)
if method == 'get_video' or method == 'v':
cor = d.get_m3u8_video(key, "unnamed")
return d, cor
raise HandleMethodError(d, method)
cors = []
for i, key in enumerate(kwargs['keys']):
cors.append(d.get_m3u8_video(key, f"{i}.ts"))
return d, asyncio.gather(*cors)
26 changes: 13 additions & 13 deletions bilix/download/base_downloader_part.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import asyncio
import re
from typing import Union, List, Iterable
import aiofiles
import httpx
import random
import os

from bilix.handle import Handler, HandleMethodError
from bilix.handle import Handler
from bilix.download.base_downloader import BaseDownloader
from bilix.utils import req_retry, merge_files
from bilix.log import logger


class BaseDownloaderPart(BaseDownloader):
def __init__(self, client: httpx.AsyncClient = None, videos_dir: str = 'videos', part_concurrency=10,
speed_limit: Union[float, int] = None, progress=None):
stream_retry=5, speed_limit: Union[float, int] = None, progress=None):
"""
Base Async http Content-Range Downloader
Expand All @@ -24,7 +23,8 @@ def __init__(self, client: httpx.AsyncClient = None, videos_dir: str = 'videos',
:param speed_limit:
:param progress:
"""
super(BaseDownloaderPart, self).__init__(client, videos_dir, speed_limit=speed_limit, progress=progress)
super(BaseDownloaderPart, self).__init__(
client, videos_dir, stream_retry=stream_retry, speed_limit=speed_limit, progress=progress)
self.part_concurrency = part_concurrency

async def _content_length(self, urls: List[Union[str, httpx.URL]]) -> int:
Expand All @@ -37,14 +37,13 @@ async def _content_length(self, urls: List[Union[str, httpx.URL]]) -> int:
return total

async def get_file(self, url_or_urls: Union[str, Iterable[str]],
file_name: str, task_id=None, hierarchy: str = '', retry: int = 5) -> str:
file_name: str, task_id=None, hierarchy: str = '') -> str:
"""
:param url_or_urls: file url or urls with backups
:param file_name:
:param task_id: if not provided, a new progress task will be created
:param hierarchy:
:param retry: retry times
:return: downloaded file path
"""
file_dir = f'{self.videos_dir}/{hierarchy}' if hierarchy else self.videos_dir
Expand All @@ -66,7 +65,7 @@ async def get_file(self, url_or_urls: Union[str, Iterable[str]],
end = (i + 1) * part_length - 1 if i < self.part_concurrency - 1 else total - 1
part_name = f'{file_name}-{start}-{end}'
part_names.append(part_name)
cors.append(self._get_file_part(urls, part_name, task_id, hierarchy=hierarchy, retry=retry))
cors.append(self._get_file_part(urls, part_name, task_id, hierarchy=hierarchy))
file_list = await asyncio.gather(*cors)
await merge_files(file_list, new_name=file_path)
if self.progress.tasks[task_id].finished:
Expand All @@ -75,9 +74,9 @@ async def get_file(self, url_or_urls: Union[str, Iterable[str]],
return file_path

async def _get_file_part(self, urls: List[Union[str, httpx.URL]],
part_name, task_id, times=0, hierarchy: str = '', retry: int = 5):
part_name, task_id, times=0, hierarchy: str = ''):
file_dir = f'{self.videos_dir}/{hierarchy}' if hierarchy else self.videos_dir
if times > retry:
if times > self.stream_retry:
raise Exception(f'STREAM 超过重试次数 {part_name}')
start, end = map(int, part_name.split('-')[-2:])
file_path = f'{file_dir}/{part_name}'
Expand Down Expand Up @@ -106,14 +105,15 @@ async def _get_file_part(self, urls: List[Union[str, httpx.URL]],

@Handler.register(name="Part")
def handle(kwargs):
key = kwargs['key']
method = kwargs['method']
if method == 'f' or method == 'get_file':
videos_dir = kwargs['videos_dir']
part_concurrency = kwargs['part_concurrency']
speed_limit = kwargs['speed_limit']
d = BaseDownloaderPart(videos_dir=videos_dir, part_concurrency=part_concurrency,
speed_limit=speed_limit)
file_name = key.split('/')[-1].split('?')[0]
cor = d.get_file(key, file_name)
return d, cor
cors = []
for key in kwargs['keys']:
file_name = key.split('/')[-1].split('?')[0]
cors.append(d.get_file(key, file_name))
return d, asyncio.gather(*cors)
5 changes: 3 additions & 2 deletions bilix/download/downloader_bilibili.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,22 @@


class DownloaderBilibili(BaseDownloaderPart):
def __init__(self, videos_dir='videos', sess_data='', video_concurrency=3, part_concurrency=10,
def __init__(self, videos_dir='videos', sess_data='', video_concurrency=3, part_concurrency=10, stream_retry=5,
speed_limit: Union[float, int] = None, progress=None):
"""
:param videos_dir: 下载到哪个目录,默认当前目录下的为videos中,如果路径不存在将自动创建
:param sess_data: 有条件的用户填写大会员凭证,填写后可下载大会员资源
:param video_concurrency: 限制最大同时下载的视频数量
:param part_concurrency: 每个媒体的分段并发数
:param stream_retry:
:param speed_limit: 下载速度限制,单位B/s
:param progress: 进度对象,不提供则使用rich命令行进度
"""
client = httpx.AsyncClient(**api.dft_client_settings)
client.cookies.set('SESSDATA', sess_data)
super(DownloaderBilibili, self).__init__(client, videos_dir, part_concurrency, speed_limit=speed_limit,
progress=progress)
stream_retry=stream_retry, progress=progress)
self.speed_limit = speed_limit
self.v_sema = asyncio.Semaphore(video_concurrency)
self._cate_meta = None
Expand Down
8 changes: 4 additions & 4 deletions bilix/download/downloader_cctv.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@


class DownloaderCctv(BaseDownloaderM3u8):
def __init__(self, videos_dir='videos', video_concurrency=3, part_concurrency=10,
def __init__(self, videos_dir='videos', video_concurrency=3, part_concurrency=10, stream_retry=5,
speed_limit: Union[float, int] = None, progress=None):
client = httpx.AsyncClient(**api.dft_client_settings)
super(DownloaderCctv, self).__init__(client, videos_dir, video_concurrency, part_concurrency,
speed_limit=speed_limit, progress=progress)
stream_retry=stream_retry, speed_limit=speed_limit, progress=progress)

async def get_series(self, url: str, quality=0, hierarchy=True):
pid, vide, vida = await api.get_id(self.client, url)
Expand All @@ -37,9 +37,9 @@ async def get_video(self, url_or_pid: str, quality=0, hierarchy=''):

@Handler.register(name='CCTV')
def handle(kwargs):
key = kwargs['key']
keys = kwargs['keys']
method = kwargs['method']
if 'cctv' in key:
if 'cctv' in keys[0]:
d = DownloaderCctv
if method == 's' or method == 'get_series':
m = d.get_series
Expand Down
11 changes: 6 additions & 5 deletions bilix/download/downloader_douyin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@


class DownloaderDouyin(BaseDownloaderPart):
def __init__(self, videos_dir='videos', part_concurrency=10, speed_limit: Union[float, int] = None, progress=None):
def __init__(self, videos_dir='videos', part_concurrency=10, stream_retry=5,
speed_limit: Union[float, int] = None, progress=None):
client = httpx.AsyncClient(**api.dft_client_settings)
super(DownloaderDouyin, self).__init__(client, videos_dir, part_concurrency,
super(DownloaderDouyin, self).__init__(client, videos_dir, part_concurrency, stream_retry=stream_retry,
speed_limit=speed_limit, progress=progress)

async def get_video(self, url: str, image=False):
Expand All @@ -26,9 +27,9 @@ async def get_video(self, url: str, image=False):

@Handler.register(name='抖音')
def handle(kwargs):
key = kwargs['key']
method = kwargs['method']
if 'douyin' in key:
keys = kwargs['keys']
if 'douyin' in keys[0]:
method = kwargs['method']
d = DownloaderDouyin
if method == 'v' or method == 'get_video':
m = d.get_video
Expand Down
19 changes: 9 additions & 10 deletions bilix/download/downloader_hanime1.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@


class DownloaderHanime1(BaseDownloaderPart):
def __init__(self, videos_dir: str = "videos", speed_limit: Union[float, int] = None, progress=None):
def __init__(self, videos_dir: str = "videos", stream_retry=5,
speed_limit: Union[float, int] = None, progress=None):
client = httpx.AsyncClient(**api.dft_client_settings)
super(DownloaderHanime1, self).__init__(client, videos_dir, speed_limit=speed_limit, progress=progress)
super(DownloaderHanime1, self).__init__(client, videos_dir, speed_limit=speed_limit,
stream_retry=stream_retry, progress=progress)

async def get_video(self, url: str, image=False):
video_info = await api.get_video_info(self.client, url)
Expand All @@ -21,14 +23,11 @@ async def get_video(self, url: str, image=False):

@Handler.register('hanime1')
def handle(kwargs):
keys = kwargs['keys']
method = kwargs['method']
key = kwargs['key']
videos_dir = kwargs['videos_dir']
image = kwargs['image']
speed_limit = kwargs['speed_limit']
if 'hanime1' in key:
d = DownloaderHanime1(videos_dir=videos_dir, speed_limit=speed_limit)
if 'hanime1' in keys[0]:
d = DownloaderHanime1
if method == 'get_video' or method == 'v':
cor = d.get_video(key, image=image)
return d, cor
m = d.get_video
return d, m
raise HandleMethodError(d, method)
Loading

0 comments on commit 1b34fc6

Please sign in to comment.