Showing
13 changed files
with
362 additions
and
4 deletions
| ... | @@ -12,7 +12,41 @@ xj-marketing This project aims to automate the posting of videos to various soci | ... | @@ -12,7 +12,41 @@ xj-marketing This project aims to automate the posting of videos to various soci |
| 12 | - [x] 小红书 | 12 | - [x] 小红书 |
| 13 | - [x] 快手 | 13 | - [x] 快手 |
| 14 | - [ ] 百家号 | 14 | - [ ] 百家号 |
| 15 | - - [ ] 网易号 | 15 | + - [ ] 哔哩哔哩 |
| 16 | + - [ ] 腾讯视频 | ||
| 17 | + - [ ] 西瓜视频 | ||
| 18 | + - [ ] 搜狐视频 | ||
| 19 | + - [ ] 爱奇艺 | ||
| 20 | + - [ ] 皮皮虾 | ||
| 21 | + - [ ] 多多视频 | ||
| 22 | + - [ ] 美拍 | ||
| 23 | + - [ ] 腾讯微视 | ||
| 24 | + - [ ] AcFun | ||
| 25 | + - [ ] 秒拍 | ||
| 26 | + - [ ] 小红书商家 | ||
| 27 | + - [ ] 头条号 | ||
| 28 | + - [ ] 百家号 | ||
| 29 | + - [ ] 搜狐号 | ||
| 30 | + - [x] 网易号 | ||
| 31 | + - [ ] 企鹅号 | ||
| 32 | + - [ ] 知乎 | ||
| 33 | + - [ ] 一点号 | ||
| 34 | + - [ ] 大鱼号 | ||
| 35 | + - [ ] 微信公众号 | ||
| 36 | + - [ ] 新浪微博 | ||
| 37 | + - [ ] 快传号 | ||
| 38 | + - [ ] 雪球号 | ||
| 39 | + - [ ] 豆瓣 | ||
| 40 | + - [ ] 简书号 | ||
| 41 | + - [ ] 东方号 | ||
| 42 | + - [ ] 大风号 | ||
| 43 | + - [ ] 蜂网 | ||
| 44 | + - [ ] 美柚 | ||
| 45 | + - [ ] WIFI万能钥匙 | ||
| 46 | + - [ ] 易车号 | ||
| 47 | + - [ ] 车家号 | ||
| 48 | + - [ ] CSDN | ||
| 49 | + - [ ] 微博头条 | ||
| 16 | 50 | ||
| 17 | - 部分国外社交媒体: | 51 | - 部分国外社交媒体: |
| 18 | - [x] tiktok | 52 | - [x] tiktok | ... | ... |
bg.png
0 → 100644
11.3 KB
| ... | @@ -10,8 +10,9 @@ from uploader.ks_uploader.main import ks_setup, KSVideo | ... | @@ -10,8 +10,9 @@ from uploader.ks_uploader.main import ks_setup, KSVideo |
| 10 | from uploader.tencent_uploader.main import weixin_setup, TencentVideo | 10 | from uploader.tencent_uploader.main import weixin_setup, TencentVideo |
| 11 | from uploader.tk_uploader.main_chrome import tiktok_setup, TiktokVideo | 11 | from uploader.tk_uploader.main_chrome import tiktok_setup, TiktokVideo |
| 12 | from uploader.wyh_uploader.main import wyh_setup | 12 | from uploader.wyh_uploader.main import wyh_setup |
| 13 | +from uploader.toutiao_uploader.main import toutiao_setup | ||
| 13 | from utils.base_social_media import get_supported_social_media, get_cli_action, SOCIAL_MEDIA_DOUYIN, \ | 14 | from utils.base_social_media import get_supported_social_media, get_cli_action, SOCIAL_MEDIA_DOUYIN, \ |
| 14 | - SOCIAL_MEDIA_TENCENT, SOCIAL_MEDIA_TIKTOK, SOCIAL_MEDIA_KUAISHOU, SOCIAL_MEDIA_WANGYIHAO | 15 | + SOCIAL_MEDIA_TENCENT, SOCIAL_MEDIA_TIKTOK, SOCIAL_MEDIA_KUAISHOU, SOCIAL_MEDIA_WANGYIHAO, SOCIAL_MEDIA_TOUTIAO |
| 15 | from utils.constant import TencentZoneTypes | 16 | from utils.constant import TencentZoneTypes |
| 16 | from utils.files_times import get_title_and_hashtags | 17 | from utils.files_times import get_title_and_hashtags |
| 17 | 18 | ||
| ... | @@ -71,6 +72,8 @@ async def main(): | ... | @@ -71,6 +72,8 @@ async def main(): |
| 71 | await ks_setup(str(account_file), handle=True) | 72 | await ks_setup(str(account_file), handle=True) |
| 72 | elif args.platform == SOCIAL_MEDIA_WANGYIHAO: | 73 | elif args.platform == SOCIAL_MEDIA_WANGYIHAO: |
| 73 | await wyh_setup(str(account_file), handle=True) | 74 | await wyh_setup(str(account_file), handle=True) |
| 75 | + elif args.platform == SOCIAL_MEDIA_TOUTIAO: | ||
| 76 | + await toutiao_setup(str(account_file), handle=True) | ||
| 74 | 77 | ||
| 75 | elif args.action == 'upload': | 78 | elif args.action == 'upload': |
| 76 | title, tags = get_title_and_hashtags(args.video_file) | 79 | title, tags = get_title_and_hashtags(args.video_file) | ... | ... |
| ... | @@ -6,4 +6,7 @@ cf_clearance | ... | @@ -6,4 +6,7 @@ cf_clearance |
| 6 | biliup | 6 | biliup |
| 7 | xhs | 7 | xhs |
| 8 | qrcode | 8 | qrcode |
| 9 | -loguru | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| 9 | +loguru | ||
| 10 | +opencv-python | ||
| 11 | +numpy | ||
| 12 | +pandas | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
uploader/toutiao_uploader/__init__.py
0 → 100644
File mode changed
uploader/toutiao_uploader/main.py
0 → 100644
| 1 | +import os | ||
| 2 | + | ||
| 3 | +from utils.base_social_media import set_init_script | ||
| 4 | +from utils.files_times import get_absolute_path | ||
| 5 | +from utils.log import kuaishou_logger | ||
| 6 | +from playwright.async_api import Playwright, async_playwright | ||
| 7 | + | ||
| 8 | + | ||
| 9 | +async def toutiao_setup(account_file, handle=False): | ||
| 10 | + print(account_file) | ||
| 11 | + account_file = get_absolute_path(account_file, "wyh_uploader") | ||
| 12 | + if not os.path.exists(account_file): | ||
| 13 | + if not handle: | ||
| 14 | + return False | ||
| 15 | + kuaishou_logger.info('[+] cookie文件不存在或已失效,即将自动打开浏览器,请扫码登录,登陆后会自动生成cookie文件') | ||
| 16 | + await get_wyh_cookie(account_file) | ||
| 17 | + else: | ||
| 18 | + await open_wyh_main_page(account_file) | ||
| 19 | + return True | ||
| 20 | + | ||
| 21 | + | ||
| 22 | +async def open_wyh_main_page(account_file): | ||
| 23 | + async with async_playwright() as playwright: | ||
| 24 | + options = { | ||
| 25 | + 'args': [ | ||
| 26 | + '--lang en-GB' | ||
| 27 | + ], | ||
| 28 | + 'headless': False, # Set headless option here | ||
| 29 | + } | ||
| 30 | + # Make sure to run headed. | ||
| 31 | + browser = await playwright.chromium.launch(**options) | ||
| 32 | + # Setup context however you like. | ||
| 33 | + context = await browser.new_context(storage_state=account_file) # Pass any options | ||
| 34 | + context = await set_init_script(context) | ||
| 35 | + # Pause the page, and start recording manually. | ||
| 36 | + page = await context.new_page() | ||
| 37 | + await page.goto("https://mp.toutiao.com") | ||
| 38 | + await page.pause() | ||
| 39 | + browser.close() | ||
| 40 | + | ||
| 41 | + | ||
| 42 | +async def get_wyh_cookie(account_file): | ||
| 43 | + print("get_wyh_cookie") | ||
| 44 | + async with async_playwright() as playwright: | ||
| 45 | + options = { | ||
| 46 | + 'args': [ | ||
| 47 | + '--lang en-GB' | ||
| 48 | + ], | ||
| 49 | + 'headless': False, # Set headless option here | ||
| 50 | + } | ||
| 51 | + # Make sure to run headed. | ||
| 52 | + browser = await playwright.chromium.launch(**options) | ||
| 53 | + # Setup context however you like. | ||
| 54 | + context = await browser.new_context() # Pass any options | ||
| 55 | + context = await set_init_script(context) | ||
| 56 | + # Pause the page, and start recording manually. | ||
| 57 | + page = await context.new_page() | ||
| 58 | + await page.goto("https://mp.toutiao.com") | ||
| 59 | + | ||
| 60 | + # 手动授权登录 | ||
| 61 | + # await page.pause() | ||
| 62 | + # 点击调试器的继续,保存cookie | ||
| 63 | + # await context.storage_state(path=account_file) | ||
| 64 | + | ||
| 65 | + # 自动登陆 | ||
| 66 | + await page.wait_for_timeout(5000) | ||
| 67 | + await page.get_by_text('密码登录').click() | ||
| 68 | + await page.wait_for_timeout(5000) | ||
| 69 | + | ||
| 70 | + await page.get_by_placeholder("手机号/邮箱").fill('18610534668') | ||
| 71 | + print("输入账号成功") | ||
| 72 | + await page.wait_for_timeout(1000) | ||
| 73 | + await page.get_by_placeholder('密码').fill("Liuyihong1023@") | ||
| 74 | + await page.wait_for_timeout(1000) | ||
| 75 | + await page.locator("//*[@class='web-login-confirm-info__checkbox']").click() | ||
| 76 | + await page.wait_for_timeout(1000) | ||
| 77 | + await page.locator("//*[@class='web-login-button']").click() | ||
| 78 | + await page.wait_for_timeout(10000) | ||
| 79 | + # 点击调试器的继续,保存cookie | ||
| 80 | + await context.storage_state(path=account_file) |
utils/CaptchaCv2.py
0 → 100644
| 1 | +import random | ||
| 2 | +import cv2 | ||
| 3 | +import numpy as np | ||
| 4 | +import pandas as pd | ||
| 5 | +import math | ||
| 6 | +import os | ||
| 7 | + | ||
| 8 | + | ||
| 9 | +# x方向一阶导中值 | ||
| 10 | +def get_dx_median(dx, x, y, w, h): | ||
| 11 | + return np.median(dx[y:(y + h), x]) | ||
| 12 | + | ||
| 13 | + | ||
| 14 | +# 预处理 | ||
| 15 | +def pre_process(img_path): | ||
| 16 | + img = cv2.imread(img_path, 1) | ||
| 17 | + img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 转成灰度图像 | ||
| 18 | + _, binary = cv2.threshold(img_gray, 127, 255, cv2.THRESH_BINARY) # 将灰度图像转成二值图像 | ||
| 19 | + contours, hierarchy = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE) # 查找轮廓 | ||
| 20 | + | ||
| 21 | + rect_area = [] | ||
| 22 | + rect_arc_length = [] | ||
| 23 | + cnt_infos = {} | ||
| 24 | + | ||
| 25 | + for i, cnt in enumerate(contours): | ||
| 26 | + if cv2.contourArea(cnt) < 5000 or cv2.contourArea(cnt) > 25000: | ||
| 27 | + continue | ||
| 28 | + | ||
| 29 | + x, y, w, h = cv2.boundingRect(cnt) | ||
| 30 | + cnt_infos[i] = {'rect_area': w * h, # 矩形面积 | ||
| 31 | + 'rect_arclength': 2 * (w + h), # 矩形周长 | ||
| 32 | + 'cnt_area': cv2.contourArea(cnt), # 轮廓面积 | ||
| 33 | + 'cnt_arclength': cv2.arcLength(cnt, True), # 轮廓周长 | ||
| 34 | + 'cnt': cnt, # 轮廓 | ||
| 35 | + 'w': w, | ||
| 36 | + 'h': h, | ||
| 37 | + 'x': x, | ||
| 38 | + 'y': y, | ||
| 39 | + 'mean': np.mean(np.min(img[y:(y + h), x:(x + w)], axis=2)), # 矩形内像素平均 | ||
| 40 | + } | ||
| 41 | + rect_area.append(w * h) | ||
| 42 | + rect_arc_length.append(2 * (w + h)) | ||
| 43 | + dx = cv2.Sobel(img, -1, 1, 0, ksize=5) | ||
| 44 | + | ||
| 45 | + return img, dx, cnt_infos | ||
| 46 | + | ||
| 47 | + | ||
| 48 | +def qq_mark_pos(img_path): | ||
| 49 | + if not os.path.exists(img_path): | ||
| 50 | + print("文件不存在") | ||
| 51 | + return 0 | ||
| 52 | + img, dx, cnt_infos = pre_process(img_path) | ||
| 53 | + h, w = img.shape[:2] | ||
| 54 | + df = pd.DataFrame(cnt_infos).T | ||
| 55 | + df.head() | ||
| 56 | + df['dx_mean'] = df.apply(lambda x: get_dx_median(dx, x['x'], x['y'], x['w'], x['h']), axis=1) | ||
| 57 | + df['rect_ratio'] = df.apply(lambda v: v['rect_arclength'] / 4 / math.sqrt(v['rect_area'] + 1), axis=1) | ||
| 58 | + df['area_ratio'] = df.apply(lambda v: v['rect_area'] / v['cnt_area'], axis=1) | ||
| 59 | + df['score'] = df.apply(lambda x: abs(x['rect_ratio'] - 1), axis=1) | ||
| 60 | + | ||
| 61 | + result = df.query('x>0').query('area_ratio<2').query('rect_area>5000').query('rect_area<20000').sort_values( | ||
| 62 | + ['mean', 'score', 'dx_mean']).head(2) | ||
| 63 | + if len(result): | ||
| 64 | + x_left = result.x.values[0] | ||
| 65 | + # cv2.line(img, (x_left, 0), (x_left, h), color=(255, 0, 255)) | ||
| 66 | + # plt.imshow(img) | ||
| 67 | + # plt.show() | ||
| 68 | + return result | ||
| 69 | + | ||
| 70 | + | ||
| 71 | +def get_track_list(distance): | ||
| 72 | + """ | ||
| 73 | + 模拟轨迹 假装是人在操作 | ||
| 74 | + """ | ||
| 75 | + # 初速度 | ||
| 76 | + v = 0 | ||
| 77 | + # 单位时间为0.2s来统计轨迹,轨迹即0.2内的位移 | ||
| 78 | + t = 0.2 | ||
| 79 | + # 位移/轨迹列表,列表内的一个元素代表0.2s的位移 | ||
| 80 | + tracks = [] | ||
| 81 | + # 当前的位移 | ||
| 82 | + current = 0 | ||
| 83 | + # 到达mid值开始减速 | ||
| 84 | + mid = distance * 7 / 8 | ||
| 85 | + | ||
| 86 | + distance += 10 # 先滑过一点,最后再反着滑动回来 | ||
| 87 | + # a = random.randint(1,3) | ||
| 88 | + while current < distance: | ||
| 89 | + if current < mid: | ||
| 90 | + # 加速度越小,单位时间的位移越小,模拟的轨迹就越多越详细 | ||
| 91 | + a = random.randint(2, 4) # 加速运动 | ||
| 92 | + else: | ||
| 93 | + a = -random.randint(3, 5) # 减速运动 | ||
| 94 | + | ||
| 95 | + # 初速度 | ||
| 96 | + v0 = v | ||
| 97 | + # 0.2秒时间内的位移 | ||
| 98 | + s = v0 * t + 0.5 * a * (t ** 2) | ||
| 99 | + # 当前的位置 | ||
| 100 | + current += s | ||
| 101 | + # 添加到轨迹列表 | ||
| 102 | + tracks.append(round(s)) | ||
| 103 | + | ||
| 104 | + # 速度已经达到v,该速度作为下次的初速度 | ||
| 105 | + v = v0 + a * t | ||
| 106 | + | ||
| 107 | + # 反着滑动到大概准确位置 | ||
| 108 | + for i in range(4): | ||
| 109 | + tracks.append(-random.randint(2, 3)) | ||
| 110 | + for i in range(4): | ||
| 111 | + tracks.append(-random.randint(1, 3)) | ||
| 112 | + return tracks | ||
| 113 | + | ||
| 114 | + | ||
| 115 | +# if __name__ == "__main__": | ||
| 116 | +# img_path = "bg.png" | ||
| 117 | +# if not os.path.exists(img_path): | ||
| 118 | +# print("文件不存在") | ||
| 119 | +# res = qq_mark_pos(img_path) | ||
| 120 | +# x = res.x.values[0] | ||
| 121 | +# x_r = 344 * x / 699 | ||
| 122 | +# track = get_track_list(res.x.values[0]) | ||
| 123 | +# print(f'x:{x},xr:{x_r}, track:{track}') |
utils/CaptchaPasser.py
0 → 100644
| 1 | +import io | ||
| 2 | +import time | ||
| 3 | +from playwright.sync_api import sync_playwright, Route | ||
| 4 | +from CaptchaCv2 import (get_track_list, qq_mark_pos) | ||
| 5 | + | ||
| 6 | +distance = 0 | ||
| 7 | +is_reflashed_img = False | ||
| 8 | +img = "bg.png" | ||
| 9 | +retryTimes = 10 | ||
| 10 | + | ||
| 11 | + | ||
| 12 | +def handle_captcha(route: Route) -> None: | ||
| 13 | + print("handle captcha begin") | ||
| 14 | + response = route.fetch() | ||
| 15 | + if response.status == 200: | ||
| 16 | + print("handle captcha response is 200") | ||
| 17 | + buffer = response.body() | ||
| 18 | + # 下载指定规则url的验证码图片 | ||
| 19 | + if "index=1" in response.url: | ||
| 20 | + is_reflashed_img = True | ||
| 21 | + with open(img, "wb") as f: | ||
| 22 | + f.write(buffer) | ||
| 23 | + route.continue_() | ||
| 24 | + | ||
| 25 | + | ||
| 26 | +def dragbox_location(): | ||
| 27 | + for i in range(5): | ||
| 28 | + dragbox_bounding = page.frame_locator("#tcaptcha_iframe").locator( | ||
| 29 | + "#tcaptcha_drag_thumb").bounding_box() | ||
| 30 | + if dragbox_bounding is not None and dragbox_bounding["x"] > 20: | ||
| 31 | + return dragbox_bounding | ||
| 32 | + return None | ||
| 33 | + | ||
| 34 | + | ||
| 35 | +def drag_to_breach(move_distance): | ||
| 36 | + print('开始拖动滑块..') | ||
| 37 | + drag_box = dragbox_location() | ||
| 38 | + if drag_box is None: | ||
| 39 | + print('未获取到滑块位置,识别失败') | ||
| 40 | + return False | ||
| 41 | + page.mouse.move(drag_box["x"] + drag_box["width"] / 2, | ||
| 42 | + drag_box["y"] + drag_box["height"] / 2) | ||
| 43 | + page.mouse.down() | ||
| 44 | + location_x = drag_box["x"] | ||
| 45 | + for i in move_distance: | ||
| 46 | + location_x += i | ||
| 47 | + page.mouse.move(location_x, drag_box["y"]) | ||
| 48 | + page.mouse.up() | ||
| 49 | + if page.get_by_text("后重试") is not None or page.get_by_text("请控制拼图对齐缺口") is not None: | ||
| 50 | + print("识别成功") | ||
| 51 | + return True | ||
| 52 | + print('识别失败') | ||
| 53 | + return False | ||
| 54 | + | ||
| 55 | + | ||
| 56 | +def calc_distance(): | ||
| 57 | + for i in range(retryTimes): | ||
| 58 | + print(f"识别验证码距离中,当前等待轮数{i + 1}/{retryTimes}") | ||
| 59 | + try: | ||
| 60 | + res = qq_mark_pos(img) | ||
| 61 | + distance = res.x.values[0] | ||
| 62 | + if distance > 0: | ||
| 63 | + print(f"获取到缺口距离:{distance}") | ||
| 64 | + return distance | ||
| 65 | + except Exception as e: | ||
| 66 | + print(f"识别错误, 异常:{e}") | ||
| 67 | + | ||
| 68 | + | ||
| 69 | +with sync_playwright() as p: | ||
| 70 | + # browser = p.chromium.launch(channel="msedge",proxy={"server": "http://{}".format(proxy)}) | ||
| 71 | + browser = p.chromium.launch(headless=False) | ||
| 72 | + iphone_12 = p.devices["iPhone 12"] | ||
| 73 | + context = browser.new_context( | ||
| 74 | + record_video_dir="videos/", | ||
| 75 | + **iphone_12, | ||
| 76 | + ) | ||
| 77 | + page = context.new_page() | ||
| 78 | + # 下载指定规则的验证码图片 | ||
| 79 | + page.route("**/turing.captcha.qcloud.com/hycdn**", handle_captcha) | ||
| 80 | + page.goto( | ||
| 81 | + "https://wap.showstart.com/pages/passport/login/login?redirect=%252Fpages%252FmyHome%252FmyHome") | ||
| 82 | + | ||
| 83 | + page.wait_for_timeout(5000) | ||
| 84 | + | ||
| 85 | + page.get_by_role("spinbutton").fill("14445104596") | ||
| 86 | + page.get_by_text("获取验证码").click() | ||
| 87 | + | ||
| 88 | + frame = page.wait_for_selector("#tcaptcha_iframe") | ||
| 89 | + print(frame.bounding_box()) | ||
| 90 | + | ||
| 91 | + page.wait_for_timeout(5000) | ||
| 92 | + | ||
| 93 | + move_distance = None | ||
| 94 | + for i in range(retryTimes): | ||
| 95 | + print(f"滑块拖动逻辑开始,当前尝试轮数{i + 1}/{retryTimes}") | ||
| 96 | + | ||
| 97 | + # 验证码刷新 重新计算距离 | ||
| 98 | + if is_reflashed_img or move_distance is None: | ||
| 99 | + distance = calc_distance() | ||
| 100 | + page.wait_for_timeout(200) | ||
| 101 | + | ||
| 102 | + true_distance = distance * 353 / 680 | ||
| 103 | + move_distance = get_track_list(true_distance) | ||
| 104 | + print(f"获取到相对滑动距离{true_distance}, 模拟拖动列表{move_distance}") | ||
| 105 | + is_reflashed_img = False | ||
| 106 | + | ||
| 107 | + drag_result = drag_to_breach(move_distance) | ||
| 108 | + if drag_result: | ||
| 109 | + break | ||
| 110 | + | ||
| 111 | + page.wait_for_timeout(3000) | ||
| 112 | + print("识别结束,退出程序") | ||
| 113 | + # input("为方便调试,可启用此代码,避免浏览器关闭") | ||
| 114 | + browser.close() | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| ... | @@ -9,10 +9,11 @@ SOCIAL_MEDIA_TIKTOK = "tiktok" | ... | @@ -9,10 +9,11 @@ SOCIAL_MEDIA_TIKTOK = "tiktok" |
| 9 | SOCIAL_MEDIA_BILIBILI = "bilibili" | 9 | SOCIAL_MEDIA_BILIBILI = "bilibili" |
| 10 | SOCIAL_MEDIA_KUAISHOU = "kuaishou" | 10 | SOCIAL_MEDIA_KUAISHOU = "kuaishou" |
| 11 | SOCIAL_MEDIA_WANGYIHAO = 'wangyihao' | 11 | SOCIAL_MEDIA_WANGYIHAO = 'wangyihao' |
| 12 | +SOCIAL_MEDIA_TOUTIAO = 'toutiao' | ||
| 12 | 13 | ||
| 13 | 14 | ||
| 14 | def get_supported_social_media() -> List[str]: | 15 | def get_supported_social_media() -> List[str]: |
| 15 | - return [SOCIAL_MEDIA_DOUYIN, SOCIAL_MEDIA_TENCENT, SOCIAL_MEDIA_TIKTOK, SOCIAL_MEDIA_KUAISHOU,SOCIAL_MEDIA_WANGYIHAO] | 16 | + return [SOCIAL_MEDIA_DOUYIN, SOCIAL_MEDIA_TENCENT, SOCIAL_MEDIA_TIKTOK, SOCIAL_MEDIA_KUAISHOU, SOCIAL_MEDIA_WANGYIHAO, SOCIAL_MEDIA_TOUTIAO] |
| 16 | 17 | ||
| 17 | 18 | ||
| 18 | def get_cli_action() -> List[str]: | 19 | def get_cli_action() -> List[str]: | ... | ... |
videos/045d9d78bac6e6980813f72def252775.webm
0 → 100644
No preview for this file type
videos/6167f7685d0838ddba02e4b3f22d6d70.webm
0 → 100644
No preview for this file type
videos/78952c951d6d12c413de16dd533a6a63.webm
0 → 100644
No preview for this file type
videos/ebcf0ece6061dfac92b69b28a34a28ac.webm
0 → 100644
No preview for this file type
-
Please register or login to post a comment