fix: 将所有后端文件都迁移到 sau_backend 文件夹内
@@ -0,0 +1,101 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from os.path import exists
|
||||
from pathlib import Path
|
||||
|
||||
from conf import BASE_DIR
|
||||
from uploader.douyin_uploader.main import douyin_setup, DouYinVideo
|
||||
from uploader.ks_uploader.main import ks_setup, KSVideo
|
||||
from uploader.tencent_uploader.main import weixin_setup, TencentVideo
|
||||
from uploader.tk_uploader.main_chrome import tiktok_setup, TiktokVideo
|
||||
from utils.base_social_media import get_supported_social_media, get_cli_action, SOCIAL_MEDIA_DOUYIN, \
|
||||
SOCIAL_MEDIA_TENCENT, SOCIAL_MEDIA_TIKTOK, SOCIAL_MEDIA_KUAISHOU
|
||||
from utils.constant import TencentZoneTypes
|
||||
from utils.files_times import get_title_and_hashtags
|
||||
|
||||
|
||||
def parse_schedule(schedule_raw):
|
||||
if schedule_raw:
|
||||
schedule = datetime.strptime(schedule_raw, '%Y-%m-%d %H:%M')
|
||||
else:
|
||||
schedule = None
|
||||
return schedule
|
||||
|
||||
|
||||
async def main():
|
||||
# 主解析器
|
||||
parser = argparse.ArgumentParser(description="Upload video to multiple social-media.")
|
||||
parser.add_argument("platform", metavar='platform', choices=get_supported_social_media(), help="Choose social-media platform: douyin tencent tiktok kuaishou")
|
||||
|
||||
parser.add_argument("account_name", type=str, help="Account name for the platform: xiaoA")
|
||||
subparsers = parser.add_subparsers(dest="action", metavar='action', help="Choose action", required=True)
|
||||
|
||||
actions = get_cli_action()
|
||||
for action in actions:
|
||||
action_parser = subparsers.add_parser(action, help=f'{action} operation')
|
||||
if action == 'login':
|
||||
# Login 不需要额外参数
|
||||
continue
|
||||
elif action == 'upload':
|
||||
action_parser.add_argument("video_file", help="Path to the Video file")
|
||||
action_parser.add_argument("-pt", "--publish_type", type=int, choices=[0, 1],
|
||||
help="0 for immediate, 1 for scheduled", default=0)
|
||||
action_parser.add_argument('-t', '--schedule', help='Schedule UTC time in %Y-%m-%d %H:%M format')
|
||||
|
||||
# 解析命令行参数
|
||||
args = parser.parse_args()
|
||||
# 参数校验
|
||||
if args.action == 'upload':
|
||||
if not exists(args.video_file):
|
||||
raise FileNotFoundError(f'Could not find the video file at {args["video_file"]}')
|
||||
if args.publish_type == 1 and not args.schedule:
|
||||
parser.error("The schedule must must be specified for scheduled publishing.")
|
||||
|
||||
account_file = Path(BASE_DIR / "cookies" / f"{args.platform}_{args.account_name}.json")
|
||||
account_file.parent.mkdir(exist_ok=True)
|
||||
|
||||
# 根据 action 处理不同的逻辑
|
||||
if args.action == 'login':
|
||||
print(f"Logging in with account {args.account_name} on platform {args.platform}")
|
||||
if args.platform == SOCIAL_MEDIA_DOUYIN:
|
||||
await douyin_setup(str(account_file), handle=True)
|
||||
elif args.platform == SOCIAL_MEDIA_TIKTOK:
|
||||
await tiktok_setup(str(account_file), handle=True)
|
||||
elif args.platform == SOCIAL_MEDIA_TENCENT:
|
||||
await weixin_setup(str(account_file), handle=True)
|
||||
elif args.platform == SOCIAL_MEDIA_KUAISHOU:
|
||||
await ks_setup(str(account_file), handle=True)
|
||||
elif args.action == 'upload':
|
||||
title, tags = get_title_and_hashtags(args.video_file)
|
||||
video_file = args.video_file
|
||||
|
||||
if args.publish_type == 0:
|
||||
print("Uploading immediately...")
|
||||
publish_date = 0
|
||||
else:
|
||||
print("Scheduling videos...")
|
||||
publish_date = parse_schedule(args.schedule)
|
||||
|
||||
if args.platform == SOCIAL_MEDIA_DOUYIN:
|
||||
await douyin_setup(account_file, handle=False)
|
||||
app = DouYinVideo(title, video_file, tags, publish_date, account_file)
|
||||
elif args.platform == SOCIAL_MEDIA_TIKTOK:
|
||||
await tiktok_setup(account_file, handle=True)
|
||||
app = TiktokVideo(title, video_file, tags, publish_date, account_file)
|
||||
elif args.platform == SOCIAL_MEDIA_TENCENT:
|
||||
await weixin_setup(account_file, handle=True)
|
||||
category = TencentZoneTypes.LIFESTYLE.value # 标记原创需要否则不需要传
|
||||
app = TencentVideo(title, video_file, tags, publish_date, account_file, category)
|
||||
elif args.platform == SOCIAL_MEDIA_KUAISHOU:
|
||||
await ks_setup(account_file, handle=True)
|
||||
app = KSVideo(title, video_file, tags, publish_date, account_file)
|
||||
else:
|
||||
print("Wrong platform, please check your input")
|
||||
exit()
|
||||
|
||||
await app.main()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,5 @@
|
||||
from pathlib import Path
|
||||
|
||||
BASE_DIR = Path(__file__).parent.resolve()
|
||||
XHS_SERVER = "http://127.0.0.1:11901"
|
||||
LOCAL_CHROME_PATH = "C:\Program Files\Google\Chrome\Application\chrome.exe" # change me necessary! for example C:/Program Files/Google/Chrome/Application/chrome.exe
|
||||
@@ -0,0 +1,10 @@
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
from sau_backend.conf import BASE_DIR
|
||||
from sau_backend.uploader.baijiahao_uploader.main import baijiahao_setup
|
||||
|
||||
if __name__ == '__main__':
|
||||
account_file = Path(BASE_DIR / "cookies" / "baijiahao_uploader" / "account.json")
|
||||
account_file.parent.mkdir(exist_ok=True)
|
||||
cookie_setup = asyncio.run(baijiahao_setup(str(account_file), handle=True))
|
||||
@@ -0,0 +1,2 @@
|
||||
# cd uploader/bilibili_uploader
|
||||
# biliup.exe -u account.json login
|
||||
@@ -0,0 +1,10 @@
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
from sau_backend.conf import BASE_DIR
|
||||
from sau_backend.uploader.douyin_uploader.main import douyin_setup
|
||||
|
||||
if __name__ == '__main__':
|
||||
account_file = Path(BASE_DIR / "cookies" / "douyin_uploader" / "account.json")
|
||||
account_file.parent.mkdir(exist_ok=True)
|
||||
cookie_setup = asyncio.run(douyin_setup(str(account_file), handle=True))
|
||||
@@ -0,0 +1,10 @@
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
from sau_backend.conf import BASE_DIR
|
||||
from sau_backend.uploader.ks_uploader.main import ks_setup
|
||||
|
||||
if __name__ == '__main__':
|
||||
account_file = Path(BASE_DIR / "cookies" / "ks_uploader" / "account.json")
|
||||
account_file.parent.mkdir(exist_ok=True)
|
||||
cookie_setup = asyncio.run(ks_setup(str(account_file), handle=True))
|
||||
@@ -0,0 +1,10 @@
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
from sau_backend.conf import BASE_DIR
|
||||
from sau_backend.uploader.tencent_uploader.main import weixin_setup
|
||||
|
||||
if __name__ == '__main__':
|
||||
account_file = Path(BASE_DIR / "cookies" / "tencent_uploader" / "account.json")
|
||||
account_file.parent.mkdir(exist_ok=True)
|
||||
cookie_setup = asyncio.run(weixin_setup(str(account_file), handle=True))
|
||||
@@ -0,0 +1,10 @@
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
from sau_backend.conf import BASE_DIR
|
||||
from sau_backend.uploader.tk_uploader.main_chrome import tiktok_setup
|
||||
|
||||
if __name__ == '__main__':
|
||||
account_file = Path(BASE_DIR / "cookies" / "tk_uploader" / "account.json")
|
||||
account_file.parent.mkdir(exist_ok=True)
|
||||
cookie_setup = asyncio.run(tiktok_setup(str(account_file), handle=True))
|
||||
@@ -0,0 +1,27 @@
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
from sau_backend.conf import BASE_DIR
|
||||
from sau_backend.uploader.baijiahao_uploader.main import baijiahao_setup, BaiJiaHaoVideo
|
||||
from sau_backend.utils.files_times import generate_schedule_time_next_day, get_title_and_hashtags
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
filepath = Path(BASE_DIR) / "videos"
|
||||
account_file = Path(BASE_DIR / "cookies" / "baijiahao_uploader" / "account.json")
|
||||
# 获取视频目录
|
||||
folder_path = Path(filepath)
|
||||
# 获取文件夹中的所有文件
|
||||
files = list(folder_path.glob("*.mp4"))
|
||||
file_num = len(files)
|
||||
publish_datetimes = generate_schedule_time_next_day(file_num, 1, daily_times=[16])
|
||||
cookie_setup = asyncio.run(baijiahao_setup(account_file, handle=False))
|
||||
for index, file in enumerate(files):
|
||||
title, tags = get_title_and_hashtags(str(file))
|
||||
thumbnail_path = file.with_suffix('.png')
|
||||
# 打印视频文件名、标题和 hashtag
|
||||
# print(f"视频文件名:{file}")
|
||||
# print(f"标题:{title}")
|
||||
# print(f"Hashtag:{tags}")
|
||||
app = BaiJiaHaoVideo(title, file, tags, publish_datetimes[index], account_file)
|
||||
asyncio.run(app.mainAi(), debug=False)
|
||||
@@ -0,0 +1,42 @@
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from sau_backend.uploader.bilibili_uploader.main import read_cookie_json_file, extract_keys_from_json, random_emoji, BilibiliUploader
|
||||
from sau_backend.conf import BASE_DIR
|
||||
from sau_backend.utils.constant import VideoZoneTypes
|
||||
from sau_backend.utils.files_times import generate_schedule_time_next_day, get_title_and_hashtags
|
||||
|
||||
if __name__ == '__main__':
|
||||
filepath = Path(BASE_DIR) / "videos"
|
||||
# how to get cookie, see the file of get_bilibili_cookie.py.
|
||||
account_file = Path(BASE_DIR / "cookies" / "bilibili_uploader" / "account.json")
|
||||
if not account_file.exists():
|
||||
print(f"{account_file.name} 配置文件不存在")
|
||||
exit()
|
||||
cookie_data = read_cookie_json_file(account_file)
|
||||
cookie_data = extract_keys_from_json(cookie_data)
|
||||
|
||||
tid = VideoZoneTypes.SPORTS_FOOTBALL.value # 设置分区id
|
||||
# 获取视频目录
|
||||
folder_path = Path(filepath)
|
||||
# 获取文件夹中的所有文件
|
||||
files = list(folder_path.glob("*.mp4"))
|
||||
file_num = len(files)
|
||||
timestamps = generate_schedule_time_next_day(file_num, 1, daily_times=[16], timestamps=True)
|
||||
|
||||
for index, file in enumerate(files):
|
||||
title, tags = get_title_and_hashtags(str(file))
|
||||
# just avoid error, bilibili don't allow same title of video.
|
||||
title += random_emoji()
|
||||
tags_str = ','.join([tag for tag in tags])
|
||||
# 打印视频文件名、标题和 hashtag
|
||||
print(f"视频文件名:{file}")
|
||||
print(f"标题:{title}")
|
||||
print(f"Hashtag:{tags}")
|
||||
# I set desc same as title, do what u like.
|
||||
desc = title
|
||||
bili_uploader = BilibiliUploader(cookie_data, file, title, desc, tid, tags, timestamps[index])
|
||||
bili_uploader.upload()
|
||||
|
||||
# life is beautiful don't so rush. be kind be patience
|
||||
time.sleep(30)
|
||||
@@ -0,0 +1,31 @@
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
from sau_backend.conf import BASE_DIR
|
||||
from sau_backend.uploader.douyin_uploader.main import douyin_setup, DouYinVideo
|
||||
from sau_backend.utils.files_times import generate_schedule_time_next_day, get_title_and_hashtags
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
filepath = Path(BASE_DIR) / "videos"
|
||||
account_file = Path(BASE_DIR / "cookies" / "douyin_uploader" / "account.json")
|
||||
# 获取视频目录
|
||||
folder_path = Path(filepath)
|
||||
# 获取文件夹中的所有文件
|
||||
files = list(folder_path.glob("*.mp4"))
|
||||
file_num = len(files)
|
||||
publish_datetimes = generate_schedule_time_next_day(file_num, 1, daily_times=[16])
|
||||
cookie_setup = asyncio.run(douyin_setup(account_file, handle=False))
|
||||
for index, file in enumerate(files):
|
||||
title, tags = get_title_and_hashtags(str(file))
|
||||
thumbnail_path = file.with_suffix('.png')
|
||||
# 打印视频文件名、标题和 hashtag
|
||||
print(f"视频文件名:{file}")
|
||||
print(f"标题:{title}")
|
||||
print(f"Hashtag:{tags}")
|
||||
# 暂时没有时间修复封面上传,故先隐藏掉该功能
|
||||
# if thumbnail_path.exists():
|
||||
# app = DouYinVideo(title, file, tags, publish_datetimes[index], account_file, thumbnail_path=thumbnail_path)
|
||||
# else:
|
||||
app = DouYinVideo(title, file, tags, publish_datetimes[index], account_file)
|
||||
asyncio.run(app.main(), debug=False)
|
||||
@@ -0,0 +1,26 @@
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
from sau_backend.conf import BASE_DIR
|
||||
from sau_backend.uploader.ks_uploader.main import ks_setup, KSVideo
|
||||
from sau_backend.utils.files_times import generate_schedule_time_next_day, get_title_and_hashtags
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
filepath = Path(BASE_DIR) / "videos"
|
||||
account_file = Path(BASE_DIR / "cookies" / "ks_uploader" / "account.json")
|
||||
# 获取视频目录
|
||||
folder_path = Path(filepath)
|
||||
# 获取文件夹中的所有文件
|
||||
files = list(folder_path.glob("*.mp4"))
|
||||
file_num = len(files)
|
||||
publish_datetimes = generate_schedule_time_next_day(file_num, 1, daily_times=[16])
|
||||
cookie_setup = asyncio.run(ks_setup(account_file, handle=False))
|
||||
for index, file in enumerate(files):
|
||||
title, tags = get_title_and_hashtags(str(file))
|
||||
# 打印视频文件名、标题和 hashtag
|
||||
print(f"视频文件名:{file}")
|
||||
print(f"标题:{title}")
|
||||
print(f"Hashtag:{tags}")
|
||||
app = KSVideo(title, file, tags, publish_datetimes[index], account_file)
|
||||
asyncio.run(app.main(), debug=False)
|
||||
@@ -0,0 +1,28 @@
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
from sau_backend.conf import BASE_DIR
|
||||
from sau_backend.uploader.tencent_uploader.main import weixin_setup, TencentVideo
|
||||
from sau_backend.utils.constant import TencentZoneTypes
|
||||
from sau_backend.utils.files_times import generate_schedule_time_next_day, get_title_and_hashtags
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
filepath = Path(BASE_DIR) / "videos"
|
||||
account_file = Path(BASE_DIR / "cookies" / "tencent_uploader" / "account.json")
|
||||
# 获取视频目录
|
||||
folder_path = Path(filepath)
|
||||
# 获取文件夹中的所有文件
|
||||
files = list(folder_path.glob("*.mp4"))
|
||||
file_num = len(files)
|
||||
publish_datetimes = generate_schedule_time_next_day(file_num, 1, daily_times=[16])
|
||||
cookie_setup = asyncio.run(weixin_setup(account_file, handle=True))
|
||||
category = TencentZoneTypes.LIFESTYLE.value # 标记原创需要否则不需要传
|
||||
for index, file in enumerate(files):
|
||||
title, tags = get_title_and_hashtags(str(file))
|
||||
# 打印视频文件名、标题和 hashtag
|
||||
print(f"视频文件名:{file}")
|
||||
print(f"标题:{title}")
|
||||
print(f"Hashtag:{tags}")
|
||||
app = TencentVideo(title, file, tags, publish_datetimes[index], account_file, category)
|
||||
asyncio.run(app.main(), debug=False)
|
||||
@@ -0,0 +1,30 @@
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
from sau_backend.conf import BASE_DIR
|
||||
# from tk_uploader.main import tiktok_setup, TiktokVideo
|
||||
from sau_backend.uploader.tk_uploader.main_chrome import tiktok_setup, TiktokVideo
|
||||
from sau_backend.utils.files_times import generate_schedule_time_next_day, get_title_and_hashtags
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
filepath = Path(BASE_DIR) / "videos"
|
||||
account_file = Path(BASE_DIR / "cookies" / "tk_uploader" / "account.json")
|
||||
folder_path = Path(filepath)
|
||||
# get video files from folder
|
||||
files = list(folder_path.glob("*.mp4"))
|
||||
file_num = len(files)
|
||||
publish_datetimes = generate_schedule_time_next_day(file_num, 1, daily_times=[16])
|
||||
cookie_setup = asyncio.run(tiktok_setup(account_file, handle=True))
|
||||
for index, file in enumerate(files):
|
||||
title, tags = get_title_and_hashtags(str(file))
|
||||
thumbnail_path = file.with_suffix('.png')
|
||||
print(f"video_file_name:{file}")
|
||||
print(f"video_title:{title}")
|
||||
print(f"video_hashtag:{tags}")
|
||||
if thumbnail_path.exists():
|
||||
print(f"thumbnail_file_name:{thumbnail_path}")
|
||||
app = TiktokVideo(title, file, tags, publish_datetimes[index], account_file, thumbnail_path)
|
||||
else:
|
||||
app = TiktokVideo(title, file, tags, publish_datetimes[index], account_file)
|
||||
asyncio.run(app.main(), debug=False)
|
||||
@@ -0,0 +1,68 @@
|
||||
import configparser
|
||||
from pathlib import Path
|
||||
from time import sleep
|
||||
|
||||
from xhs import XhsClient
|
||||
|
||||
from sau_backend.conf import BASE_DIR
|
||||
from sau_backend.utils.files_times import generate_schedule_time_next_day, get_title_and_hashtags
|
||||
from sau_backend.uploader.xhs_uploader.main import sign_local, beauty_print
|
||||
|
||||
config = configparser.RawConfigParser()
|
||||
config.read(Path(BASE_DIR / "uploader" / "xhs_uploader" / "accounts.ini"))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
filepath = Path(BASE_DIR) / "videos"
|
||||
# 获取视频目录
|
||||
folder_path = Path(filepath)
|
||||
# 获取文件夹中的所有文件
|
||||
files = list(folder_path.glob("*.mp4"))
|
||||
file_num = len(files)
|
||||
|
||||
cookies = config['account1']['cookies']
|
||||
xhs_client = XhsClient(cookies, sign=sign_local, timeout=60)
|
||||
# auth cookie
|
||||
# 注意:该校验cookie方式可能并没那么准确
|
||||
try:
|
||||
xhs_client.get_video_first_frame_image_id("3214")
|
||||
except:
|
||||
print("cookie 失效")
|
||||
exit()
|
||||
|
||||
publish_datetimes = generate_schedule_time_next_day(file_num, 1, daily_times=[16])
|
||||
|
||||
for index, file in enumerate(files):
|
||||
title, tags = get_title_and_hashtags(str(file))
|
||||
# 加入到标题 补充标题(xhs 可以填1000字不写白不写)
|
||||
tags_str = ' '.join(['#' + tag for tag in tags])
|
||||
hash_tags_str = ''
|
||||
hash_tags = []
|
||||
|
||||
# 打印视频文件名、标题和 hashtag
|
||||
print(f"视频文件名:{file}")
|
||||
print(f"标题:{title}")
|
||||
print(f"Hashtag:{tags}")
|
||||
|
||||
topics = []
|
||||
# 获取hashtag
|
||||
for i in tags[:3]:
|
||||
topic_official = xhs_client.get_suggest_topic(i)
|
||||
if topic_official:
|
||||
topic_official[0]['type'] = 'topic'
|
||||
topic_one = topic_official[0]
|
||||
hash_tag_name = topic_one['name']
|
||||
hash_tags.append(hash_tag_name)
|
||||
topics.append(topic_one)
|
||||
|
||||
hash_tags_str = ' ' + ' '.join(['#' + tag + '[话题]#' for tag in hash_tags])
|
||||
|
||||
note = xhs_client.create_video_note(title=title[:20], video_path=str(file),
|
||||
desc=title + tags_str + hash_tags_str,
|
||||
topics=topics,
|
||||
is_private=False,
|
||||
post_time=publish_datetimes[index].strftime("%Y-%m-%d %H:%M:%S"))
|
||||
|
||||
beauty_print(note)
|
||||
# 强制休眠30s,避免风控(必要)
|
||||
sleep(30)
|
||||
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 274 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 396 KiB |
|
After Width: | Height: | Size: 901 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,9 @@
|
||||
requests
|
||||
playwright
|
||||
eventlet
|
||||
schedule
|
||||
cf_clearance
|
||||
biliup
|
||||
xhs
|
||||
qrcode
|
||||
loguru
|
||||
@@ -0,0 +1,5 @@
|
||||
from pathlib import Path
|
||||
|
||||
from sau_backend.conf import BASE_DIR
|
||||
|
||||
Path(BASE_DIR / "cookies").mkdir(exist_ok=True)
|
||||
@@ -0,0 +1,505 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import random
|
||||
from datetime import datetime
|
||||
|
||||
from playwright.async_api import Playwright, async_playwright, Page
|
||||
import os
|
||||
import time
|
||||
import asyncio
|
||||
|
||||
from sau_backend.conf import LOCAL_CHROME_PATH
|
||||
from sau_backend.utils.base_social_media import set_init_script
|
||||
from sau_backend.utils.log import baijiahao_logger
|
||||
from sau_backend.utils.network import async_retry
|
||||
|
||||
|
||||
async def baijiahao_cookie_gen(account_file):
|
||||
async with async_playwright() as playwright:
|
||||
options = {
|
||||
'args': [
|
||||
'--lang en-GB'
|
||||
],
|
||||
'headless': False, # Set headless option here
|
||||
}
|
||||
# Make sure to run headed.
|
||||
browser = await playwright.chromium.launch(**options)
|
||||
# Setup context however you like.
|
||||
context = await browser.new_context() # Pass any options
|
||||
context = await set_init_script(context)
|
||||
# Pause the page, and start recording manually.
|
||||
page = await context.new_page()
|
||||
await page.goto("https://baijiahao.baidu.com/builder/theme/bjh/login")
|
||||
await page.pause()
|
||||
# 点击调试器的继续,保存cookie
|
||||
await context.storage_state(path=account_file)
|
||||
baijiahao_logger.success("cookie saved")
|
||||
|
||||
|
||||
async def cookie_auth(account_file):
|
||||
async with async_playwright() as playwright:
|
||||
browser = await playwright.chromium.launch(headless=True)
|
||||
context = await browser.new_context(storage_state=account_file)
|
||||
context = await set_init_script(context)
|
||||
# 创建一个新的页面
|
||||
page = await context.new_page()
|
||||
# 访问指定的 URL
|
||||
await page.goto("https://baijiahao.baidu.com/builder/rc/home")
|
||||
await page.wait_for_timeout(timeout=5000)
|
||||
|
||||
if await page.get_by_text('注册/登录百家号').count():
|
||||
baijiahao_logger.error("等待5秒 cookie 失效")
|
||||
return False
|
||||
else:
|
||||
baijiahao_logger.success("[+] cookie 有效")
|
||||
return True
|
||||
|
||||
|
||||
async def baijiahao_setup(account_file, handle=False):
|
||||
if not os.path.exists(account_file) or not await cookie_auth(account_file):
|
||||
if not handle:
|
||||
return False
|
||||
baijiahao_logger.error("cookie文件不存在或已失效,即将自动打开浏览器,请扫码登录,登陆后会自动生成cookie文件")
|
||||
await baijiahao_cookie_gen(account_file)
|
||||
return True
|
||||
|
||||
class BaiJiaHaoVideo(object):
|
||||
def __init__(self, title, file_path, tags, publish_date: datetime, account_file, proxy_setting=None):
|
||||
self.title = title # 视频标题
|
||||
self.file_path = file_path
|
||||
self.tags = tags
|
||||
self.publish_date = publish_date
|
||||
self.account_file = account_file
|
||||
self.date_format = '%Y年%m月%d日 %H:%M'
|
||||
self.local_executable_path = LOCAL_CHROME_PATH
|
||||
self.proxy_setting = proxy_setting
|
||||
|
||||
async def set_schedule_time(self, page, publish_date):
|
||||
"""
|
||||
todo 时间选择,日后在处理 百家号的时间选择不准确,目前是随机
|
||||
"""
|
||||
publish_date_day = f"{publish_date.month}月{publish_date.day}日" if publish_date.day >9 else f"{publish_date.month}月0{publish_date.day}日"
|
||||
publish_date_hour = f"{publish_date.hour}点"
|
||||
publish_date_min = f"{publish_date.minute}分"
|
||||
await page.wait_for_selector('div.select-wrap', timeout=5000)
|
||||
for _ in range(3):
|
||||
try:
|
||||
await page.locator('div.select-wrap').nth(0).click()
|
||||
await page.wait_for_selector('div.rc-virtual-list div.cheetah-select-item', timeout=5000)
|
||||
break
|
||||
except:
|
||||
await page.locator('div.select-wrap').nth(0).click()
|
||||
# page.locator(f'div.rc-virtual-list-holder-inner >> text={publish_date_day}').click()
|
||||
await page.wait_for_timeout(2000)
|
||||
await page.locator(f'div.rc-virtual-list div.cheetah-select-item >> text={publish_date_day}').click()
|
||||
await page.wait_for_timeout(2000)
|
||||
|
||||
# 改为随机点击一个 hour
|
||||
for _ in range(3):
|
||||
try:
|
||||
await page.locator('div.select-wrap').nth(1).click()
|
||||
await page.wait_for_selector('div.rc-virtual-list div.rc-virtual-list-holder-inner:visible', timeout=5000)
|
||||
break
|
||||
except:
|
||||
await page.locator('div.select-wrap').nth(1).click()
|
||||
await page.wait_for_timeout(2000)
|
||||
current_choice_hour = await page.locator('div.rc-virtual-list:visible div.cheetah-select-item-option').count()
|
||||
await page.wait_for_timeout(2000)
|
||||
await page.locator('div.rc-virtual-list:visible div.cheetah-select-item-option').nth(
|
||||
random.randint(1, current_choice_hour-3)).click()
|
||||
# 2024.08.05 current_choice_hour的获取可能有问题,页面有7,这里获取了10,暂时硬编码至6
|
||||
|
||||
await page.wait_for_timeout(2000)
|
||||
await page.locator("button >> text=定时发布").click()
|
||||
|
||||
|
||||
async def handle_upload_error(self, page):
|
||||
# 日后实现,目前没遇到
|
||||
return
|
||||
print("视频出错了,重新上传中")
|
||||
|
||||
async def upload(self, playwright: Playwright) -> None:
|
||||
# 使用 Chromium 浏览器启动一个浏览器实例
|
||||
browser = await playwright.chromium.launch(headless=False, executable_path=self.local_executable_path, proxy=self.proxy_setting)
|
||||
# 创建一个浏览器上下文,使用指定的 cookie 文件
|
||||
context = await browser.new_context(storage_state=f"{self.account_file}", user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.4324.150 Safari/537.36')
|
||||
# context = await set_init_script(context)
|
||||
await context.grant_permissions(['geolocation'])
|
||||
|
||||
# 创建一个新的页面
|
||||
page = await context.new_page()
|
||||
# 访问指定的 URL
|
||||
await page.goto("https://baijiahao.baidu.com/builder/rc/edit?type=videoV2", timeout=60000)
|
||||
baijiahao_logger.info(f"正在上传-------{self.title}.mp4")
|
||||
# 等待页面跳转到指定的 URL,没进入,则自动等待到超时
|
||||
baijiahao_logger.info('正在打开主页...')
|
||||
await page.wait_for_url("https://baijiahao.baidu.com/builder/rc/edit?type=videoV2", timeout=60000)
|
||||
|
||||
# 点击 "上传视频" 按钮
|
||||
await page.locator("div[class^='video-main-container'] input").set_input_files(self.file_path)
|
||||
|
||||
# 等待页面跳转到指定的 URL
|
||||
while True:
|
||||
# 判断是是否进入视频发布页面,没进入,则自动等待到超时
|
||||
try:
|
||||
await page.wait_for_selector("div#formMain:visible")
|
||||
break
|
||||
except:
|
||||
baijiahao_logger.info("正在等待进入视频发布页面...")
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# 填充标题和话题
|
||||
# 这里为了避免页面变化,故使用相对位置定位:作品标题父级右侧第一个元素的input子元素
|
||||
await asyncio.sleep(1)
|
||||
baijiahao_logger.info("正在填充标题和话题...")
|
||||
await self.add_title_tags(page)
|
||||
|
||||
upload_status = await self.uploading_video(page)
|
||||
if not upload_status:
|
||||
baijiahao_logger.error(f"发现上传出错了... 文件:{self.file_path}")
|
||||
raise
|
||||
|
||||
# 判断视频封面图是否生成成功
|
||||
while True:
|
||||
baijiahao_logger.info("正在确认封面完成, 准备去点击定时/发布...")
|
||||
if await page.locator("div.cheetah-spin-container img").count():
|
||||
baijiahao_logger.info("封面已完成,点击定时/发布...")
|
||||
break
|
||||
else:
|
||||
baijiahao_logger.info("等待封面生成...")
|
||||
await asyncio.sleep(3)
|
||||
|
||||
await self.publish_video(page, self.publish_date)
|
||||
await page.wait_for_timeout(2000)
|
||||
if await page.locator('div.passMod_dialog-container >> text=百度安全验证:visible').count():
|
||||
baijiahao_logger.error("出现验证,退出")
|
||||
raise Exception("出现验证,退出")
|
||||
await page.wait_for_url("https://baijiahao.baidu.com/builder/rc/clue**", timeout=5000)
|
||||
baijiahao_logger.success("视频发布成功")
|
||||
|
||||
await context.storage_state(path=self.account_file) # 保存cookie
|
||||
baijiahao_logger.info('cookie更新完毕!')
|
||||
await asyncio.sleep(2) # 这里延迟是为了方便眼睛直观的观看
|
||||
# 关闭浏览器上下文和浏览器实例
|
||||
await context.close()
|
||||
await browser.close()
|
||||
|
||||
|
||||
@async_retry(timeout=300) # 例如,最多重试3次,超时时间为180秒
|
||||
async def uploading_video(self, page):
|
||||
while True:
|
||||
upload_failed = await page.locator('div .cover-overlay:has-text("上传失败")').count()
|
||||
if upload_failed:
|
||||
baijiahao_logger.error("发现上传出错了...")
|
||||
# await self.handle_upload_error(page) # 假设这是处理上传错误的函数
|
||||
return False
|
||||
|
||||
uploading = await page.locator('div .cover-overlay:has-text("上传中")').count()
|
||||
if uploading:
|
||||
baijiahao_logger.info("正在上传视频中...")
|
||||
await asyncio.sleep(2) # 等待2秒再次检查
|
||||
continue
|
||||
|
||||
# 检查上传是否成功
|
||||
if not uploading and not upload_failed:
|
||||
baijiahao_logger.success("视频上传完毕")
|
||||
return True
|
||||
|
||||
async def set_schedule_publish(self, page, publish_date):
|
||||
while True:
|
||||
schedule_element = page.locator("div.op-btn-outter-content >> text=定时发布").locator("..").locator(
|
||||
'button')
|
||||
try:
|
||||
await schedule_element.click()
|
||||
await page.wait_for_selector('div.select-wrap:visible', timeout=3000)
|
||||
await page.wait_for_timeout(timeout=2000)
|
||||
baijiahao_logger.info("开始点击发布定时...")
|
||||
await self.set_schedule_time(page, publish_date)
|
||||
break
|
||||
except Exception as e:
|
||||
baijiahao_logger.error(f"定时发布失败: {e}")
|
||||
raise # 重新抛出异常,让重试装饰器捕获
|
||||
|
||||
@async_retry(timeout=300) # 例如,最多重试3次,超时时间为180秒
|
||||
async def publish_video(self, page: Page, publish_date):
|
||||
if publish_date != 0:
|
||||
# 定时发布
|
||||
await self.set_schedule_publish(page, publish_date)
|
||||
else:
|
||||
# 立即发布
|
||||
await self.direct_publish(page)
|
||||
|
||||
async def direct_publish(self, page):
|
||||
try:
|
||||
publish_button = page.locator("button >> text=发布")
|
||||
if await publish_button.count():
|
||||
await publish_button.click()
|
||||
except Exception as e:
|
||||
baijiahao_logger.error(f"直接发布视频失败: {e}")
|
||||
raise # 重新抛出异常,让重试装饰器捕获
|
||||
|
||||
async def add_title_tags(self, page):
|
||||
title_container = page.get_by_placeholder('添加标题获得更多推荐')
|
||||
if len(self.title) <= 8:
|
||||
self.title += " 你不知道的"
|
||||
await title_container.fill(self.title[:30])
|
||||
|
||||
async def main(self):
|
||||
async with async_playwright() as playwright:
|
||||
await self.upload(playwright)
|
||||
|
||||
|
||||
|
||||
# 使用 AI成片 功能
|
||||
async def ai2video(self, playwright: Playwright) -> None:
|
||||
# 使用 Chromium 浏览器启动一个浏览器实例
|
||||
browser = await playwright.chromium.launch(headless=False, executable_path=self.local_executable_path, proxy=self.proxy_setting)
|
||||
# 创建一个浏览器上下文,使用指定的 cookie 文件
|
||||
context = await browser.new_context(
|
||||
viewport={"width": 1600, "height": 900},
|
||||
storage_state=f"{self.account_file}",
|
||||
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.4324.150 Safari/537.36'
|
||||
)
|
||||
# context = await set_init_script(context)
|
||||
await context.grant_permissions(['geolocation'])
|
||||
|
||||
# 创建一个新的页面
|
||||
page = await context.new_page()
|
||||
# 访问指定的 URL
|
||||
await page.goto("https://aigc.baidu.com/make", timeout=60000)
|
||||
# 等待页面跳转到指定的 URL,没进入,则自动等待到超时
|
||||
baijiahao_logger.info('正在打开主页...')
|
||||
await page.wait_for_url("https://aigc.baidu.com/make", timeout=60000)
|
||||
|
||||
# 点击"全网"标签
|
||||
await page.locator('div.rounded-lg.border:has-text("全网")').click()
|
||||
await asyncio.sleep(1) # 这里延迟是为了方便眼睛直观的观看
|
||||
|
||||
# 点击 "上传视频" 按钮
|
||||
# await page.locator("div[class^='video-main-container'] input").set_input_files(self.file_path)
|
||||
|
||||
# region 操作处
|
||||
|
||||
# 生成日期时间键名(格式:ai2video_YYYYMMDDHHMM)
|
||||
now = datetime.now()
|
||||
datetime_str = now.strftime("%Y%m%d%H%M")
|
||||
processed_key = "ai2video_processed_titles"
|
||||
batch_key = f"ai2video_{datetime_str}"
|
||||
|
||||
# 初始化LocalStorage
|
||||
await page.evaluate(f"""
|
||||
if (!localStorage.getItem("{processed_key}")) {{
|
||||
localStorage.setItem("{processed_key}", JSON.stringify([]));
|
||||
}}
|
||||
if (!localStorage.getItem("{batch_key}")) {{
|
||||
localStorage.setItem("{batch_key}", JSON.stringify([]));
|
||||
}}
|
||||
""")
|
||||
|
||||
# 定位新闻列表容器(转义特殊CSS字符)
|
||||
container_selector = '.overflow-auto.flex-grow.h-0.saas-scrollbar.mt\-\[-4px\].pl\-\[24px\].pr\-\[10px\].pb\-\[18px\]'
|
||||
news_items = await page.locator(container_selector).locator('div.py\-\[6px\].group.cursor-pointer').all()
|
||||
|
||||
for item in news_items:
|
||||
try:
|
||||
# 获取新闻标题
|
||||
title_elem = item.locator('div.flex.text-gray-darker.items-center.relative.pr\-\[56px\] > span')
|
||||
title = await title_elem.text_content()
|
||||
if not title:
|
||||
continue
|
||||
|
||||
# 检查是否已处理过
|
||||
is_processed = await page.evaluate(
|
||||
f"""title => {{
|
||||
const processedList = JSON.parse(localStorage.getItem("{processed_key}") || "[]");
|
||||
return processedList.includes(title);
|
||||
}}""",
|
||||
title
|
||||
)
|
||||
|
||||
if is_processed:
|
||||
print(f"[跳过] {title}")
|
||||
continue
|
||||
|
||||
# 悬停显示按钮(根据HTML结构,按钮在悬停时显示)
|
||||
await item.hover()
|
||||
|
||||
# 点击生成文案按钮
|
||||
button = item.locator('button:has-text("生成文案")')
|
||||
await button.click()
|
||||
print(f"[点击] {title}")
|
||||
|
||||
# 等待30秒
|
||||
# await page.wait_for_timeout(30000)
|
||||
print(f"[等待完成] {title}")
|
||||
|
||||
# 监听"一键成片"按钮
|
||||
print(f"[开始监听] 一键成片按钮")
|
||||
should_exit_while_loop = False # 添加标志变量
|
||||
while True:
|
||||
# 定位"一键成片"按钮
|
||||
one_key_button = page.locator("button:has-text('一键成片')")
|
||||
|
||||
# 检查按钮是否存在
|
||||
if await one_key_button.count() > 0:
|
||||
# 检查按钮是否有disabled属性
|
||||
is_disabled = await one_key_button.get_attribute("disabled")
|
||||
|
||||
if is_disabled is None:
|
||||
# 按钮不再被禁用,点击它
|
||||
print(f"[发现可点击按钮] 一键成片")
|
||||
await one_key_button.click() # 先点击一键成片按钮
|
||||
|
||||
# 等待可能出现的"温馨提示"窗口
|
||||
print(f"[检查] 是否出现温馨提示窗口")
|
||||
await page.wait_for_timeout(2000) # 等待2秒,让窗口有时间显示
|
||||
|
||||
try:
|
||||
# 检查是否存在"温馨提示"窗口,设置较短的超时时间
|
||||
tip_window = page.locator("div:has-text('温馨提示') >> visible=true")
|
||||
if await tip_window.count() > 0:
|
||||
print(f"[发现] 温馨提示窗口")
|
||||
|
||||
# 定位并点击"知道了"按钮,设置较短的超时时间
|
||||
know_button = page.locator("button:has-text('知道了')")
|
||||
if await know_button.count() > 0:
|
||||
try:
|
||||
# 设置较短的超时时间进行点击
|
||||
await know_button.click(timeout=5000)
|
||||
print(f"[已点击] 知道了按钮")
|
||||
except Exception as e:
|
||||
print(f"[警告] 点击知道了按钮时出错: {str(e)}")
|
||||
else:
|
||||
print(f"[警告] 未找到知道了按钮")
|
||||
else:
|
||||
print(f"[信息] 未出现温馨提示窗口,继续执行")
|
||||
except Exception as e:
|
||||
print(f"[警告] 处理温馨提示窗口时出错: {str(e)}")
|
||||
# 继续执行,不要因为这个错误中断流程
|
||||
|
||||
# 记录到LocalStorage前打印日志
|
||||
print(f"[开始记录] 准备将标题 '{title}' 记录到LocalStorage")
|
||||
|
||||
# 记录到LocalStorage
|
||||
await page.evaluate(
|
||||
f"""
|
||||
(title, processedKey, batchKey) => {{
|
||||
// 更新已处理列表
|
||||
const processedList = JSON.parse(localStorage.getItem(processedKey) || "[]");
|
||||
if (!processedList.includes(title)) {{
|
||||
processedList.push(title);
|
||||
localStorage.setItem(processedKey, JSON.stringify(processedList));
|
||||
}}
|
||||
|
||||
// 更新当前批次记录
|
||||
const batchList = JSON.parse(localStorage.getItem(batchKey) || "[]");
|
||||
if (!batchList.includes(title)) {{
|
||||
batchList.push(title);
|
||||
localStorage.setItem(batchKey, JSON.stringify(batchList));
|
||||
}}
|
||||
}}
|
||||
""",
|
||||
title, processed_key, batch_key
|
||||
)
|
||||
|
||||
# 记录完成后打印日志
|
||||
print(f"[记录完成] 标题 '{title}' 已成功记录到LocalStorage")
|
||||
|
||||
print(f"[记录完成] {title}")
|
||||
|
||||
# 监听新打开的标签页
|
||||
print(f"[监听] 等待新标签页打开")
|
||||
# 获取当前所有页面
|
||||
current_pages = context.pages
|
||||
current_page_count = len(current_pages)
|
||||
|
||||
# 等待新标签页打开(最多等待10秒)
|
||||
new_page = None
|
||||
max_wait_time = 10 # 最大等待时间(秒)
|
||||
start_time = time.time()
|
||||
|
||||
while time.time() - start_time < max_wait_time:
|
||||
# 获取最新的页面列表
|
||||
pages = context.pages
|
||||
# 如果页面数量增加,说明新标签页已打开
|
||||
if len(pages) > current_page_count:
|
||||
# 获取最新打开的页面(通常是列表中的最后一个)
|
||||
new_page = pages[-1]
|
||||
print(f"[发现] 新标签页已打开")
|
||||
break
|
||||
# 短暂等待后再次检查
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# 如果找到新标签页,获取其标题和URL并保存
|
||||
if new_page:
|
||||
# 等待页面加载完成
|
||||
try:
|
||||
await new_page.wait_for_load_state("domcontentloaded", timeout=5000)
|
||||
# 获取页面标题和URL
|
||||
page_title = await new_page.title()
|
||||
page_url = new_page.url
|
||||
|
||||
print(f"[获取] 标题: {page_title}")
|
||||
print(f"[获取] URL: {page_url}")
|
||||
|
||||
# 将标题和URL保存到url.txt文件
|
||||
with open("url.txt", "a", encoding="utf-8") as f:
|
||||
f.write(f"{page_title}\n{page_url}\n\n")
|
||||
|
||||
print(f"[保存] 标题和URL已保存到url.txt")
|
||||
|
||||
# 等待5秒后关闭新标签页
|
||||
print(f"[等待] 5秒后将关闭新标签页")
|
||||
await asyncio.sleep(5)
|
||||
await new_page.close()
|
||||
print(f"[关闭] 新标签页已关闭")
|
||||
except Exception as e:
|
||||
print(f"[错误] 处理新标签页时出错: {str(e)}")
|
||||
try:
|
||||
# 尝试关闭页面,即使出错
|
||||
await new_page.close()
|
||||
print(f"[关闭] 新标签页已关闭(出错后)")
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
print(f"[警告] 未检测到新标签页打开")
|
||||
|
||||
# 跳出整个while循环
|
||||
print(f"[操作] 跳出所有循环,不再处理其他新闻")
|
||||
should_exit_while_loop = True # 设置标志变量
|
||||
break # 跳出while循环
|
||||
|
||||
# 检查是否需要跳出while循环
|
||||
if should_exit_while_loop:
|
||||
break
|
||||
|
||||
# 每秒检查一次按钮状态
|
||||
await page.wait_for_timeout(1000)
|
||||
|
||||
# 检查是否需要跳出for循环
|
||||
if should_exit_while_loop:
|
||||
print(f"[操作] 跳出for循环,完全结束处理")
|
||||
break # 跳出for循环
|
||||
except Exception as e:
|
||||
print(f"处理新闻时出错: {str(e)}")
|
||||
continue
|
||||
|
||||
|
||||
# endregion 操作处
|
||||
|
||||
print(f"[循环完成] 准备关闭浏览器")
|
||||
|
||||
# 暂停 1000s
|
||||
await asyncio.sleep(1000) # 这里延迟是为了方便眼睛直观的观看
|
||||
|
||||
# 退出前保存 storage 信息
|
||||
await context.storage_state(path=self.account_file) # 保存cookie
|
||||
baijiahao_logger.info('cookie更新完毕!')
|
||||
await asyncio.sleep(2) # 这里延迟是为了方便眼睛直观的观看
|
||||
# 关闭浏览器上下文和浏览器实例
|
||||
await context.close()
|
||||
await browser.close()
|
||||
|
||||
|
||||
async def mainAi(self):
|
||||
async with async_playwright() as playwright:
|
||||
await self.ai2video(playwright)
|
||||
@@ -0,0 +1,5 @@
|
||||
from pathlib import Path
|
||||
|
||||
from sau_backend.conf import BASE_DIR
|
||||
|
||||
Path(BASE_DIR / "cookies" / "bilibili_uploader").mkdir(exist_ok=True)
|
||||
@@ -0,0 +1,79 @@
|
||||
import json
|
||||
import pathlib
|
||||
import random
|
||||
from biliup.plugins.bili_webup import BiliBili, Data
|
||||
|
||||
from sau_backend.utils.log import bilibili_logger
|
||||
|
||||
|
||||
def extract_keys_from_json(data):
|
||||
"""Extract specified keys from the provided JSON data."""
|
||||
keys_to_extract = ["SESSDATA", "bili_jct", "DedeUserID__ckMd5", "DedeUserID", "access_token"]
|
||||
extracted_data = {}
|
||||
|
||||
# Extracting cookie data
|
||||
for cookie in data['cookie_info']['cookies']:
|
||||
if cookie['name'] in keys_to_extract:
|
||||
extracted_data[cookie['name']] = cookie['value']
|
||||
|
||||
# Extracting access_token
|
||||
if "access_token" in data['token_info']:
|
||||
extracted_data['access_token'] = data['token_info']['access_token']
|
||||
|
||||
return extracted_data
|
||||
|
||||
|
||||
def read_cookie_json_file(filepath: pathlib.Path):
|
||||
with open(filepath, 'r', encoding='utf-8') as file:
|
||||
content = json.load(file)
|
||||
return content
|
||||
|
||||
|
||||
def random_emoji():
|
||||
emoji_list = ["🍏", "🍎", "🍊", "🍋", "🍌", "🍉", "🍇", "🍓", "🍈", "🍒", "🍑", "🍍", "🥭", "🥥", "🥝",
|
||||
"🍅", "🍆", "🥑", "🥦", "🥒", "🥬", "🌶", "🌽", "🥕", "🥔", "🍠", "🥐", "🍞", "🥖", "🥨", "🥯", "🧀", "🥚", "🍳", "🥞",
|
||||
"🥓", "🥩", "🍗", "🍖", "🌭", "🍔", "🍟", "🍕", "🥪", "🥙", "🌮", "🌯", "🥗", "🥘", "🥫", "🍝", "🍜", "🍲", "🍛", "🍣",
|
||||
"🍱", "🥟", "🍤", "🍙", "🍚", "🍘", "🍥", "🥮", "🥠", "🍢", "🍡", "🍧", "🍨", "🍦", "🥧", "🍰", "🎂", "🍮", "🍭", "🍬",
|
||||
"🍫", "🍿", "🧂", "🍩", "🍪", "🌰", "🥜", "🍯", "🥛", "🍼", "☕️", "🍵", "🥤", "🍶", "🍻", "🥂", "🍷", "🥃", "🍸", "🍹",
|
||||
"🍾", "🥄", "🍴", "🍽", "🥣", "🥡", "🥢"]
|
||||
return random.choice(emoji_list)
|
||||
|
||||
|
||||
class BilibiliUploader(object):
|
||||
def __init__(self, cookie_data, file: pathlib.Path, title, desc, tid, tags, dtime):
|
||||
self.upload_thread_num = 3
|
||||
self.copyright = 1
|
||||
self.lines = 'AUTO'
|
||||
self.cookie_data = cookie_data
|
||||
self.file = file
|
||||
self.title = title
|
||||
self.desc = desc
|
||||
self.tid = tid
|
||||
self.tags = tags
|
||||
self.dtime = dtime
|
||||
self._init_data()
|
||||
|
||||
def _init_data(self):
|
||||
self.data = Data()
|
||||
self.data.copyright = self.copyright
|
||||
self.data.title = self.title
|
||||
self.data.desc = self.desc
|
||||
self.data.tid = self.tid
|
||||
self.data.set_tag(self.tags)
|
||||
self.data.dtime = self.dtime
|
||||
|
||||
def upload(self):
|
||||
with BiliBili(self.data) as bili:
|
||||
bili.login_by_cookies(self.cookie_data)
|
||||
bili.access_token = self.cookie_data.get('access_token')
|
||||
video_part = bili.upload_file(str(self.file), lines=self.lines,
|
||||
tasks=self.upload_thread_num) # 上传视频,默认线路AUTO自动选择,线程数量3。
|
||||
video_part['title'] = self.title
|
||||
self.data.append(video_part)
|
||||
ret = bili.submit() # 提交视频
|
||||
if ret.get('code') == 0:
|
||||
bilibili_logger.success(f'[+] {self.file.name}上传 成功')
|
||||
return True
|
||||
else:
|
||||
bilibili_logger.error(f'[-] {self.file.name}上传 失败, error messge: {ret.get("message")}')
|
||||
return False
|
||||
@@ -0,0 +1,5 @@
|
||||
from pathlib import Path
|
||||
|
||||
from sau_backend.conf import BASE_DIR
|
||||
|
||||
Path(BASE_DIR / "cookies" / "douyin_uploader").mkdir(exist_ok=True)
|
||||
@@ -0,0 +1,249 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import datetime
|
||||
|
||||
from playwright.async_api import Playwright, async_playwright, Page
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
from sau_backend.conf import LOCAL_CHROME_PATH
|
||||
from sau_backend.utils.base_social_media import set_init_script
|
||||
from sau_backend.utils.log import douyin_logger
|
||||
|
||||
|
||||
async def cookie_auth(account_file):
|
||||
async with async_playwright() as playwright:
|
||||
browser = await playwright.chromium.launch(headless=True)
|
||||
context = await browser.new_context(storage_state=account_file)
|
||||
context = await set_init_script(context)
|
||||
# 创建一个新的页面
|
||||
page = await context.new_page()
|
||||
# 访问指定的 URL
|
||||
await page.goto("https://creator.douyin.com/creator-micro/content/upload")
|
||||
try:
|
||||
await page.wait_for_url("https://creator.douyin.com/creator-micro/content/upload", timeout=5000)
|
||||
except:
|
||||
print("[+] 等待5秒 cookie 失效")
|
||||
await context.close()
|
||||
await browser.close()
|
||||
return False
|
||||
# 2024.06.17 抖音创作者中心改版
|
||||
if await page.get_by_text('手机号登录').count() or await page.get_by_text('扫码登录').count():
|
||||
print("[+] 等待5秒 cookie 失效")
|
||||
return False
|
||||
else:
|
||||
print("[+] cookie 有效")
|
||||
return True
|
||||
|
||||
|
||||
async def douyin_setup(account_file, handle=False):
|
||||
if not os.path.exists(account_file) or not await cookie_auth(account_file):
|
||||
if not handle:
|
||||
# Todo alert message
|
||||
return False
|
||||
douyin_logger.info('[+] cookie文件不存在或已失效,即将自动打开浏览器,请扫码登录,登陆后会自动生成cookie文件')
|
||||
await douyin_cookie_gen(account_file)
|
||||
return True
|
||||
|
||||
|
||||
async def douyin_cookie_gen(account_file):
|
||||
async with async_playwright() as playwright:
|
||||
options = {
|
||||
'headless': False
|
||||
}
|
||||
# Make sure to run headed.
|
||||
browser = await playwright.chromium.launch(**options)
|
||||
# Setup context however you like.
|
||||
context = await browser.new_context() # Pass any options
|
||||
context = await set_init_script(context)
|
||||
# Pause the page, and start recording manually.
|
||||
page = await context.new_page()
|
||||
await page.goto("https://creator.douyin.com/")
|
||||
await page.pause()
|
||||
# 点击调试器的继续,保存cookie
|
||||
await context.storage_state(path=account_file)
|
||||
|
||||
|
||||
class DouYinVideo(object):
|
||||
def __init__(self, title, file_path, tags, publish_date: datetime, account_file, thumbnail_path=None):
|
||||
self.title = title # 视频标题
|
||||
self.file_path = file_path
|
||||
self.tags = tags
|
||||
self.publish_date = publish_date
|
||||
self.account_file = account_file
|
||||
self.date_format = '%Y年%m月%d日 %H:%M'
|
||||
self.local_executable_path = LOCAL_CHROME_PATH
|
||||
self.thumbnail_path = thumbnail_path
|
||||
|
||||
async def set_schedule_time_douyin(self, page, publish_date):
|
||||
# 选择包含特定文本内容的 label 元素
|
||||
label_element = page.locator("[class^='radio']:has-text('定时发布')")
|
||||
# 在选中的 label 元素下点击 checkbox
|
||||
await label_element.click()
|
||||
await asyncio.sleep(1)
|
||||
publish_date_hour = publish_date.strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
await asyncio.sleep(1)
|
||||
await page.locator('.semi-input[placeholder="日期和时间"]').click()
|
||||
await page.keyboard.press("Control+KeyA")
|
||||
await page.keyboard.type(str(publish_date_hour))
|
||||
await page.keyboard.press("Enter")
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def handle_upload_error(self, page):
|
||||
douyin_logger.info('视频出错了,重新上传中')
|
||||
await page.locator('div.progress-div [class^="upload-btn-input"]').set_input_files(self.file_path)
|
||||
|
||||
async def upload(self, playwright: Playwright) -> None:
|
||||
# 使用 Chromium 浏览器启动一个浏览器实例
|
||||
if self.local_executable_path:
|
||||
browser = await playwright.chromium.launch(headless=False, executable_path=self.local_executable_path)
|
||||
else:
|
||||
browser = await playwright.chromium.launch(headless=False)
|
||||
# 创建一个浏览器上下文,使用指定的 cookie 文件
|
||||
context = await browser.new_context(storage_state=f"{self.account_file}")
|
||||
context = await set_init_script(context)
|
||||
|
||||
# 创建一个新的页面
|
||||
page = await context.new_page()
|
||||
# 访问指定的 URL
|
||||
await page.goto("https://creator.douyin.com/creator-micro/content/upload")
|
||||
douyin_logger.info(f'[+]正在上传-------{self.title}.mp4')
|
||||
# 等待页面跳转到指定的 URL,没进入,则自动等待到超时
|
||||
douyin_logger.info(f'[-] 正在打开主页...')
|
||||
await page.wait_for_url("https://creator.douyin.com/creator-micro/content/upload")
|
||||
# 点击 "上传视频" 按钮
|
||||
await page.locator("div[class^='container'] input").set_input_files(self.file_path)
|
||||
|
||||
# 等待页面跳转到指定的 URL 2025.01.08修改在原有基础上兼容两种页面
|
||||
while True:
|
||||
try:
|
||||
# 尝试等待第一个 URL
|
||||
await page.wait_for_url(
|
||||
"https://creator.douyin.com/creator-micro/content/publish?enter_from=publish_page", timeout=3000)
|
||||
douyin_logger.info("[+] 成功进入version_1发布页面!")
|
||||
break # 成功进入页面后跳出循环
|
||||
except Exception:
|
||||
try:
|
||||
# 如果第一个 URL 超时,再尝试等待第二个 URL
|
||||
await page.wait_for_url(
|
||||
"https://creator.douyin.com/creator-micro/content/post/video?enter_from=publish_page",
|
||||
timeout=3000)
|
||||
douyin_logger.info("[+] 成功进入version_2发布页面!")
|
||||
|
||||
break # 成功进入页面后跳出循环
|
||||
except:
|
||||
print(" [-] 超时未进入视频发布页面,重新尝试...")
|
||||
await asyncio.sleep(0.5) # 等待 0.5 秒后重新尝试
|
||||
# 填充标题和话题
|
||||
# 检查是否存在包含输入框的元素
|
||||
# 这里为了避免页面变化,故使用相对位置定位:作品标题父级右侧第一个元素的input子元素
|
||||
await asyncio.sleep(1)
|
||||
douyin_logger.info(f' [-] 正在填充标题和话题...')
|
||||
title_container = page.get_by_text('作品标题').locator("..").locator("xpath=following-sibling::div[1]").locator("input")
|
||||
if await title_container.count():
|
||||
await title_container.fill(self.title[:30])
|
||||
else:
|
||||
titlecontainer = page.locator(".notranslate")
|
||||
await titlecontainer.click()
|
||||
await page.keyboard.press("Backspace")
|
||||
await page.keyboard.press("Control+KeyA")
|
||||
await page.keyboard.press("Delete")
|
||||
await page.keyboard.type(self.title)
|
||||
await page.keyboard.press("Enter")
|
||||
css_selector = ".zone-container"
|
||||
for index, tag in enumerate(self.tags, start=1):
|
||||
await page.type(css_selector, "#" + tag)
|
||||
await page.press(css_selector, "Space")
|
||||
douyin_logger.info(f'总共添加{len(self.tags)}个话题')
|
||||
|
||||
while True:
|
||||
# 判断重新上传按钮是否存在,如果不存在,代表视频正在上传,则等待
|
||||
try:
|
||||
# 新版:定位重新上传
|
||||
number = await page.locator('[class^="long-card"] div:has-text("重新上传")').count()
|
||||
if number > 0:
|
||||
douyin_logger.success(" [-]视频上传完毕")
|
||||
break
|
||||
else:
|
||||
douyin_logger.info(" [-] 正在上传视频中...")
|
||||
await asyncio.sleep(2)
|
||||
|
||||
if await page.locator('div.progress-div > div:has-text("上传失败")').count():
|
||||
douyin_logger.error(" [-] 发现上传出错了... 准备重试")
|
||||
await self.handle_upload_error(page)
|
||||
except:
|
||||
douyin_logger.info(" [-] 正在上传视频中...")
|
||||
await asyncio.sleep(2)
|
||||
|
||||
#上传视频封面
|
||||
await self.set_thumbnail(page, self.thumbnail_path)
|
||||
|
||||
# 更换可见元素
|
||||
await self.set_location(page, "杭州市")
|
||||
|
||||
# 頭條/西瓜
|
||||
third_part_element = '[class^="info"] > [class^="first-part"] div div.semi-switch'
|
||||
# 定位是否有第三方平台
|
||||
if await page.locator(third_part_element).count():
|
||||
# 检测是否是已选中状态
|
||||
if 'semi-switch-checked' not in await page.eval_on_selector(third_part_element, 'div => div.className'):
|
||||
await page.locator(third_part_element).locator('input.semi-switch-native-control').click()
|
||||
|
||||
if self.publish_date != 0:
|
||||
await self.set_schedule_time_douyin(page, self.publish_date)
|
||||
|
||||
# 判断视频是否发布成功
|
||||
while True:
|
||||
# 判断视频是否发布成功
|
||||
try:
|
||||
publish_button = page.get_by_role('button', name="发布", exact=True)
|
||||
if await publish_button.count():
|
||||
await publish_button.click()
|
||||
await page.wait_for_url("https://creator.douyin.com/creator-micro/content/manage**",
|
||||
timeout=3000) # 如果自动跳转到作品页面,则代表发布成功
|
||||
douyin_logger.success(" [-]视频发布成功")
|
||||
break
|
||||
except:
|
||||
douyin_logger.info(" [-] 视频正在发布中...")
|
||||
await page.screenshot(full_page=True)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
await context.storage_state(path=self.account_file) # 保存cookie
|
||||
douyin_logger.success(' [-]cookie更新完毕!')
|
||||
await asyncio.sleep(2) # 这里延迟是为了方便眼睛直观的观看
|
||||
# 关闭浏览器上下文和浏览器实例
|
||||
await context.close()
|
||||
await browser.close()
|
||||
|
||||
async def set_thumbnail(self, page: Page, thumbnail_path: str):
|
||||
if thumbnail_path:
|
||||
await page.click('text="选择封面"')
|
||||
await page.wait_for_selector("div.semi-modal-content:visible")
|
||||
await page.click('text="设置竖封面"')
|
||||
await page.wait_for_timeout(2000) # 等待2秒
|
||||
# 定位到上传区域并点击
|
||||
await page.locator("div[class^='semi-upload upload'] >> input.semi-upload-hidden-input").set_input_files(thumbnail_path)
|
||||
await page.wait_for_timeout(2000) # 等待2秒
|
||||
await page.locator("div[class^='extractFooter'] button:visible:has-text('完成')").click()
|
||||
# finish_confirm_element = page.locator("div[class^='confirmBtn'] >> div:has-text('完成')")
|
||||
# if await finish_confirm_element.count():
|
||||
# await finish_confirm_element.click()
|
||||
# await page.locator("div[class^='footer'] button:has-text('完成')").click()
|
||||
|
||||
async def set_location(self, page: Page, location: str = "杭州市"):
|
||||
# todo supoort location later
|
||||
# await page.get_by_text('添加标签').locator("..").locator("..").locator("xpath=following-sibling::div").locator(
|
||||
# "div.semi-select-single").nth(0).click()
|
||||
await page.locator('div.semi-select span:has-text("输入地理位置")').click()
|
||||
await page.keyboard.press("Backspace")
|
||||
await page.wait_for_timeout(2000)
|
||||
await page.keyboard.type(location)
|
||||
await page.wait_for_selector('div[role="listbox"] [role="option"]', timeout=5000)
|
||||
await page.locator('div[role="listbox"] [role="option"]').first.click()
|
||||
|
||||
async def main(self):
|
||||
async with async_playwright() as playwright:
|
||||
await self.upload(playwright)
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
from pathlib import Path
|
||||
|
||||
from sau_backend.conf import BASE_DIR
|
||||
|
||||
Path(BASE_DIR / "cookies" / "ks_uploader").mkdir(exist_ok=True)
|
||||
@@ -0,0 +1,214 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import datetime
|
||||
|
||||
from playwright.async_api import Playwright, async_playwright
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
from sau_backend.conf import LOCAL_CHROME_PATH
|
||||
from sau_backend.utils.base_social_media import set_init_script
|
||||
from sau_backend.utils.files_times import get_absolute_path
|
||||
from sau_backend.utils.log import kuaishou_logger
|
||||
|
||||
|
||||
async def cookie_auth(account_file):
|
||||
async with async_playwright() as playwright:
|
||||
browser = await playwright.chromium.launch(headless=True)
|
||||
context = await browser.new_context(storage_state=account_file)
|
||||
context = await set_init_script(context)
|
||||
# 创建一个新的页面
|
||||
page = await context.new_page()
|
||||
# 访问指定的 URL
|
||||
await page.goto("https://cp.kuaishou.com/article/publish/video")
|
||||
try:
|
||||
await page.wait_for_selector("div.names div.container div.name:text('机构服务')", timeout=5000) # 等待5秒
|
||||
|
||||
kuaishou_logger.info("[+] 等待5秒 cookie 失效")
|
||||
return False
|
||||
except:
|
||||
kuaishou_logger.success("[+] cookie 有效")
|
||||
return True
|
||||
|
||||
|
||||
async def ks_setup(account_file, handle=False):
|
||||
account_file = get_absolute_path(account_file, "ks_uploader")
|
||||
if not os.path.exists(account_file) or not await cookie_auth(account_file):
|
||||
if not handle:
|
||||
return False
|
||||
kuaishou_logger.info('[+] cookie文件不存在或已失效,即将自动打开浏览器,请扫码登录,登陆后会自动生成cookie文件')
|
||||
await get_ks_cookie(account_file)
|
||||
return True
|
||||
|
||||
|
||||
async def get_ks_cookie(account_file):
|
||||
async with async_playwright() as playwright:
|
||||
options = {
|
||||
'args': [
|
||||
'--lang en-GB'
|
||||
],
|
||||
'headless': False, # Set headless option here
|
||||
}
|
||||
# Make sure to run headed.
|
||||
browser = await playwright.chromium.launch(**options)
|
||||
# Setup context however you like.
|
||||
context = await browser.new_context() # Pass any options
|
||||
context = await set_init_script(context)
|
||||
# Pause the page, and start recording manually.
|
||||
page = await context.new_page()
|
||||
await page.goto("https://cp.kuaishou.com")
|
||||
await page.pause()
|
||||
# 点击调试器的继续,保存cookie
|
||||
await context.storage_state(path=account_file)
|
||||
|
||||
|
||||
class KSVideo(object):
|
||||
def __init__(self, title, file_path, tags, publish_date: datetime, account_file):
|
||||
self.title = title # 视频标题
|
||||
self.file_path = file_path
|
||||
self.tags = tags
|
||||
self.publish_date = publish_date
|
||||
self.account_file = account_file
|
||||
self.date_format = '%Y-%m-%d %H:%M'
|
||||
self.local_executable_path = LOCAL_CHROME_PATH
|
||||
|
||||
async def handle_upload_error(self, page):
|
||||
kuaishou_logger.error("视频出错了,重新上传中")
|
||||
await page.locator('div.progress-div [class^="upload-btn-input"]').set_input_files(self.file_path)
|
||||
|
||||
async def upload(self, playwright: Playwright) -> None:
|
||||
# 使用 Chromium 浏览器启动一个浏览器实例
|
||||
print(self.local_executable_path)
|
||||
if self.local_executable_path:
|
||||
browser = await playwright.chromium.launch(
|
||||
headless=False,
|
||||
executable_path=self.local_executable_path,
|
||||
)
|
||||
else:
|
||||
browser = await playwright.chromium.launch(
|
||||
headless=False
|
||||
) # 创建一个浏览器上下文,使用指定的 cookie 文件
|
||||
context = await browser.new_context(storage_state=f"{self.account_file}")
|
||||
context = await set_init_script(context)
|
||||
context.on("close", lambda: context.storage_state(path=self.account_file))
|
||||
|
||||
# 创建一个新的页面
|
||||
page = await context.new_page()
|
||||
# 访问指定的 URL
|
||||
await page.goto("https://cp.kuaishou.com/article/publish/video")
|
||||
kuaishou_logger.info('正在上传-------{}.mp4'.format(self.title))
|
||||
# 等待页面跳转到指定的 URL,没进入,则自动等待到超时
|
||||
kuaishou_logger.info('正在打开主页...')
|
||||
await page.wait_for_url("https://cp.kuaishou.com/article/publish/video")
|
||||
# 点击 "上传视频" 按钮
|
||||
upload_button = page.locator("button[class^='_upload-btn']")
|
||||
await upload_button.wait_for(state='visible') # 确保按钮可见
|
||||
|
||||
async with page.expect_file_chooser() as fc_info:
|
||||
await upload_button.click()
|
||||
file_chooser = await fc_info.value
|
||||
await file_chooser.set_files(self.file_path)
|
||||
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# if not await page.get_by_text("封面编辑").count():
|
||||
# raise Exception("似乎没有跳转到到编辑页面")
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# 等待按钮可交互
|
||||
new_feature_button = page.locator('button[type="button"] span:text("我知道了")')
|
||||
if await new_feature_button.count() > 0:
|
||||
await new_feature_button.click()
|
||||
|
||||
kuaishou_logger.info("正在填充标题和话题...")
|
||||
await page.get_by_text("描述").locator("xpath=following-sibling::div").click()
|
||||
kuaishou_logger.info("clear existing title")
|
||||
await page.keyboard.press("Backspace")
|
||||
await page.keyboard.press("Control+KeyA")
|
||||
await page.keyboard.press("Delete")
|
||||
kuaishou_logger.info("filling new title")
|
||||
await page.keyboard.type(self.title)
|
||||
await page.keyboard.press("Enter")
|
||||
|
||||
# 快手只能添加3个话题
|
||||
for index, tag in enumerate(self.tags[:3], start=1):
|
||||
kuaishou_logger.info("正在添加第%s个话题" % index)
|
||||
await page.keyboard.type(f"#{tag} ")
|
||||
await asyncio.sleep(2)
|
||||
|
||||
max_retries = 60 # 设置最大重试次数,最大等待时间为 2 分钟
|
||||
retry_count = 0
|
||||
|
||||
while retry_count < max_retries:
|
||||
try:
|
||||
# 获取包含 '上传中' 文本的元素数量
|
||||
number = await page.locator("text=上传中").count()
|
||||
|
||||
if number == 0:
|
||||
kuaishou_logger.success("视频上传完毕")
|
||||
break
|
||||
else:
|
||||
if retry_count % 5 == 0:
|
||||
kuaishou_logger.info("正在上传视频中...")
|
||||
await asyncio.sleep(2)
|
||||
except Exception as e:
|
||||
kuaishou_logger.error(f"检查上传状态时发生错误: {e}")
|
||||
await asyncio.sleep(2) # 等待 2 秒后重试
|
||||
retry_count += 1
|
||||
|
||||
if retry_count == max_retries:
|
||||
kuaishou_logger.warning("超过最大重试次数,视频上传可能未完成。")
|
||||
|
||||
# 定时任务
|
||||
if self.publish_date != 0:
|
||||
await self.set_schedule_time(page, self.publish_date)
|
||||
|
||||
# 判断视频是否发布成功
|
||||
while True:
|
||||
try:
|
||||
publish_button = page.get_by_text("发布", exact=True)
|
||||
if await publish_button.count() > 0:
|
||||
await publish_button.click()
|
||||
|
||||
await asyncio.sleep(1)
|
||||
confirm_button = page.get_by_text("确认发布")
|
||||
if await confirm_button.count() > 0:
|
||||
await confirm_button.click()
|
||||
|
||||
# 等待页面跳转,确认发布成功
|
||||
await page.wait_for_url(
|
||||
"https://cp.kuaishou.com/article/manage/video?status=2&from=publish",
|
||||
timeout=5000,
|
||||
)
|
||||
kuaishou_logger.success("视频发布成功")
|
||||
break
|
||||
except Exception as e:
|
||||
kuaishou_logger.info(f"视频正在发布中... 错误: {e}")
|
||||
await page.screenshot(full_page=True)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
await context.storage_state(path=self.account_file) # 保存cookie
|
||||
kuaishou_logger.info('cookie更新完毕!')
|
||||
await asyncio.sleep(2) # 这里延迟是为了方便眼睛直观的观看
|
||||
# 关闭浏览器上下文和浏览器实例
|
||||
await context.close()
|
||||
await browser.close()
|
||||
|
||||
async def main(self):
|
||||
async with async_playwright() as playwright:
|
||||
await self.upload(playwright)
|
||||
|
||||
async def set_schedule_time(self, page, publish_date):
|
||||
kuaishou_logger.info("click schedule")
|
||||
publish_date_hour = publish_date.strftime("%Y-%m-%d %H:%M:%S")
|
||||
await page.locator("label:text('发布时间')").locator('xpath=following-sibling::div').locator(
|
||||
'.ant-radio-input').nth(1).click()
|
||||
await asyncio.sleep(1)
|
||||
|
||||
await page.locator('div.ant-picker-input input[placeholder="选择日期时间"]').click()
|
||||
await asyncio.sleep(1)
|
||||
|
||||
await page.keyboard.press("Control+KeyA")
|
||||
await page.keyboard.type(str(publish_date_hour))
|
||||
await page.keyboard.press("Enter")
|
||||
await asyncio.sleep(1)
|
||||
@@ -0,0 +1,5 @@
|
||||
from pathlib import Path
|
||||
|
||||
from sau_backend.conf import BASE_DIR
|
||||
|
||||
Path(BASE_DIR / "cookies" / "tencent_uploader").mkdir(exist_ok=True)
|
||||
@@ -0,0 +1,267 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import datetime
|
||||
|
||||
from playwright.async_api import Playwright, async_playwright
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
from sau_backend.conf import LOCAL_CHROME_PATH
|
||||
from sau_backend.utils.base_social_media import set_init_script
|
||||
from sau_backend.utils.files_times import get_absolute_path
|
||||
from sau_backend.utils.log import tencent_logger
|
||||
|
||||
|
||||
def format_str_for_short_title(origin_title: str) -> str:
|
||||
# 定义允许的特殊字符
|
||||
allowed_special_chars = "《》“”:+?%°"
|
||||
|
||||
# 移除不允许的特殊字符
|
||||
filtered_chars = [char if char.isalnum() or char in allowed_special_chars else ' ' if char == ',' else '' for
|
||||
char in origin_title]
|
||||
formatted_string = ''.join(filtered_chars)
|
||||
|
||||
# 调整字符串长度
|
||||
if len(formatted_string) > 16:
|
||||
# 截断字符串
|
||||
formatted_string = formatted_string[:16]
|
||||
elif len(formatted_string) < 6:
|
||||
# 使用空格来填充字符串
|
||||
formatted_string += ' ' * (6 - len(formatted_string))
|
||||
|
||||
return formatted_string
|
||||
|
||||
|
||||
async def cookie_auth(account_file):
|
||||
async with async_playwright() as playwright:
|
||||
browser = await playwright.chromium.launch(headless=True)
|
||||
context = await browser.new_context(storage_state=account_file)
|
||||
context = await set_init_script(context)
|
||||
# 创建一个新的页面
|
||||
page = await context.new_page()
|
||||
# 访问指定的 URL
|
||||
await page.goto("https://channels.weixin.qq.com/platform/post/create")
|
||||
try:
|
||||
await page.wait_for_selector('div.title-name:has-text("微信小店")', timeout=5000) # 等待5秒
|
||||
tencent_logger.error("[+] 等待5秒 cookie 失效")
|
||||
return False
|
||||
except:
|
||||
tencent_logger.success("[+] cookie 有效")
|
||||
return True
|
||||
|
||||
|
||||
async def get_tencent_cookie(account_file):
|
||||
async with async_playwright() as playwright:
|
||||
options = {
|
||||
'args': [
|
||||
'--lang en-GB'
|
||||
],
|
||||
'headless': False, # Set headless option here
|
||||
}
|
||||
# Make sure to run headed.
|
||||
browser = await playwright.chromium.launch(**options)
|
||||
# Setup context however you like.
|
||||
context = await browser.new_context() # Pass any options
|
||||
# Pause the page, and start recording manually.
|
||||
context = await set_init_script(context)
|
||||
page = await context.new_page()
|
||||
await page.goto("https://channels.weixin.qq.com")
|
||||
await page.pause()
|
||||
# 点击调试器的继续,保存cookie
|
||||
await context.storage_state(path=account_file)
|
||||
|
||||
|
||||
async def weixin_setup(account_file, handle=False):
|
||||
account_file = get_absolute_path(account_file, "tencent_uploader")
|
||||
if not os.path.exists(account_file) or not await cookie_auth(account_file):
|
||||
if not handle:
|
||||
# Todo alert message
|
||||
return False
|
||||
tencent_logger.info('[+] cookie文件不存在或已失效,即将自动打开浏览器,请扫码登录,登陆后会自动生成cookie文件')
|
||||
await get_tencent_cookie(account_file)
|
||||
return True
|
||||
|
||||
|
||||
class TencentVideo(object):
|
||||
def __init__(self, title, file_path, tags, publish_date: datetime, account_file, category=None):
|
||||
self.title = title # 视频标题
|
||||
self.file_path = file_path
|
||||
self.tags = tags
|
||||
self.publish_date = publish_date
|
||||
self.account_file = account_file
|
||||
self.category = category
|
||||
self.local_executable_path = LOCAL_CHROME_PATH
|
||||
|
||||
async def set_schedule_time_tencent(self, page, publish_date):
|
||||
label_element = page.locator("label").filter(has_text="定时").nth(1)
|
||||
await label_element.click()
|
||||
|
||||
await page.click('input[placeholder="请选择发表时间"]')
|
||||
|
||||
str_month = str(publish_date.month) if publish_date.month > 9 else "0" + str(publish_date.month)
|
||||
current_month = str_month + "月"
|
||||
# 获取当前的月份
|
||||
page_month = await page.inner_text('span.weui-desktop-picker__panel__label:has-text("月")')
|
||||
|
||||
# 检查当前月份是否与目标月份相同
|
||||
if page_month != current_month:
|
||||
await page.click('button.weui-desktop-btn__icon__right')
|
||||
|
||||
# 获取页面元素
|
||||
elements = await page.query_selector_all('table.weui-desktop-picker__table a')
|
||||
|
||||
# 遍历元素并点击匹配的元素
|
||||
for element in elements:
|
||||
if 'weui-desktop-picker__disabled' in await element.evaluate('el => el.className'):
|
||||
continue
|
||||
text = await element.inner_text()
|
||||
if text.strip() == str(publish_date.day):
|
||||
await element.click()
|
||||
break
|
||||
|
||||
# 输入小时部分(假设选择11小时)
|
||||
await page.click('input[placeholder="请选择时间"]')
|
||||
await page.keyboard.press("Control+KeyA")
|
||||
await page.keyboard.type(str(publish_date.hour))
|
||||
|
||||
# 选择标题栏(令定时时间生效)
|
||||
await page.locator("div.input-editor").click()
|
||||
|
||||
async def handle_upload_error(self, page):
|
||||
tencent_logger.info("视频出错了,重新上传中")
|
||||
await page.locator('div.media-status-content div.tag-inner:has-text("删除")').click()
|
||||
await page.get_by_role('button', name="删除", exact=True).click()
|
||||
file_input = page.locator('input[type="file"]')
|
||||
await file_input.set_input_files(self.file_path)
|
||||
|
||||
async def upload(self, playwright: Playwright) -> None:
|
||||
# 使用 Chromium (这里使用系统内浏览器,用chromium 会造成h264错误
|
||||
browser = await playwright.chromium.launch(headless=False, executable_path=self.local_executable_path)
|
||||
# 创建一个浏览器上下文,使用指定的 cookie 文件
|
||||
context = await browser.new_context(storage_state=f"{self.account_file}")
|
||||
context = await set_init_script(context)
|
||||
|
||||
# 创建一个新的页面
|
||||
page = await context.new_page()
|
||||
# 访问指定的 URL
|
||||
await page.goto("https://channels.weixin.qq.com/platform/post/create")
|
||||
tencent_logger.info(f'[+]正在上传-------{self.title}.mp4')
|
||||
# 等待页面跳转到指定的 URL,没进入,则自动等待到超时
|
||||
await page.wait_for_url("https://channels.weixin.qq.com/platform/post/create")
|
||||
# await page.wait_for_selector('input[type="file"]', timeout=10000)
|
||||
file_input = page.locator('input[type="file"]')
|
||||
await file_input.set_input_files(self.file_path)
|
||||
# 填充标题和话题
|
||||
await self.add_title_tags(page)
|
||||
# 添加商品
|
||||
# await self.add_product(page)
|
||||
# 合集功能
|
||||
await self.add_collection(page)
|
||||
# 原创选择
|
||||
await self.add_original(page)
|
||||
# 检测上传状态
|
||||
await self.detect_upload_status(page)
|
||||
if self.publish_date != 0:
|
||||
await self.set_schedule_time_tencent(page, self.publish_date)
|
||||
# 添加短标题
|
||||
await self.add_short_title(page)
|
||||
|
||||
await self.click_publish(page)
|
||||
|
||||
await context.storage_state(path=f"{self.account_file}") # 保存cookie
|
||||
tencent_logger.success(' [-]cookie更新完毕!')
|
||||
await asyncio.sleep(2) # 这里延迟是为了方便眼睛直观的观看
|
||||
# 关闭浏览器上下文和浏览器实例
|
||||
await context.close()
|
||||
await browser.close()
|
||||
|
||||
async def add_short_title(self, page):
|
||||
short_title_element = page.get_by_text("短标题", exact=True).locator("..").locator(
|
||||
"xpath=following-sibling::div").locator(
|
||||
'span input[type="text"]')
|
||||
if await short_title_element.count():
|
||||
short_title = format_str_for_short_title(self.title)
|
||||
await short_title_element.fill(short_title)
|
||||
|
||||
async def click_publish(self, page):
|
||||
while True:
|
||||
try:
|
||||
publish_buttion = page.locator('div.form-btns button:has-text("发表")')
|
||||
if await publish_buttion.count():
|
||||
await publish_buttion.click()
|
||||
await page.wait_for_url("https://channels.weixin.qq.com/platform/post/list", timeout=1500)
|
||||
tencent_logger.success(" [-]视频发布成功")
|
||||
break
|
||||
except Exception as e:
|
||||
current_url = page.url
|
||||
if "https://channels.weixin.qq.com/platform/post/list" in current_url:
|
||||
tencent_logger.success(" [-]视频发布成功")
|
||||
break
|
||||
else:
|
||||
tencent_logger.exception(f" [-] Exception: {e}")
|
||||
tencent_logger.info(" [-] 视频正在发布中...")
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
async def detect_upload_status(self, page):
|
||||
while True:
|
||||
# 匹配删除按钮,代表视频上传完毕,如果不存在,代表视频正在上传,则等待
|
||||
try:
|
||||
# 匹配删除按钮,代表视频上传完毕
|
||||
if "weui-desktop-btn_disabled" not in await page.get_by_role("button", name="发表").get_attribute(
|
||||
'class'):
|
||||
tencent_logger.info(" [-]视频上传完毕")
|
||||
break
|
||||
else:
|
||||
tencent_logger.info(" [-] 正在上传视频中...")
|
||||
await asyncio.sleep(2)
|
||||
# 出错了视频出错
|
||||
if await page.locator('div.status-msg.error').count() and await page.locator(
|
||||
'div.media-status-content div.tag-inner:has-text("删除")').count():
|
||||
tencent_logger.error(" [-] 发现上传出错了...准备重试")
|
||||
await self.handle_upload_error(page)
|
||||
except:
|
||||
tencent_logger.info(" [-] 正在上传视频中...")
|
||||
await asyncio.sleep(2)
|
||||
|
||||
async def add_title_tags(self, page):
|
||||
await page.locator("div.input-editor").click()
|
||||
await page.keyboard.type(self.title)
|
||||
await page.keyboard.press("Enter")
|
||||
for index, tag in enumerate(self.tags, start=1):
|
||||
await page.keyboard.type("#" + tag)
|
||||
await page.keyboard.press("Space")
|
||||
tencent_logger.info(f"成功添加hashtag: {len(self.tags)}")
|
||||
|
||||
async def add_collection(self, page):
|
||||
collection_elements = page.get_by_text("添加到合集").locator("xpath=following-sibling::div").locator(
|
||||
'.option-list-wrap > div')
|
||||
if await collection_elements.count() > 1:
|
||||
await page.get_by_text("添加到合集").locator("xpath=following-sibling::div").click()
|
||||
await collection_elements.first.click()
|
||||
|
||||
async def add_original(self, page):
|
||||
if await page.get_by_label("视频为原创").count():
|
||||
await page.get_by_label("视频为原创").check()
|
||||
# 检查 "我已阅读并同意 《视频号原创声明使用条款》" 元素是否存在
|
||||
label_locator = await page.locator('label:has-text("我已阅读并同意 《视频号原创声明使用条款》")').is_visible()
|
||||
if label_locator:
|
||||
await page.get_by_label("我已阅读并同意 《视频号原创声明使用条款》").check()
|
||||
await page.get_by_role("button", name="声明原创").click()
|
||||
# 2023年11月20日 wechat更新: 可能新账号或者改版账号,出现新的选择页面
|
||||
if await page.locator('div.label span:has-text("声明原创")').count() and self.category:
|
||||
# 因处罚无法勾选原创,故先判断是否可用
|
||||
if not await page.locator('div.declare-original-checkbox input.ant-checkbox-input').is_disabled():
|
||||
await page.locator('div.declare-original-checkbox input.ant-checkbox-input').click()
|
||||
if not await page.locator(
|
||||
'div.declare-original-dialog label.ant-checkbox-wrapper.ant-checkbox-wrapper-checked:visible').count():
|
||||
await page.locator('div.declare-original-dialog input.ant-checkbox-input:visible').click()
|
||||
if await page.locator('div.original-type-form > div.form-label:has-text("原创类型"):visible').count():
|
||||
await page.locator('div.form-content:visible').click() # 下拉菜单
|
||||
await page.locator(
|
||||
f'div.form-content:visible ul.weui-desktop-dropdown__list li.weui-desktop-dropdown__list-ele:has-text("{self.category}")').first.click()
|
||||
await page.wait_for_timeout(1000)
|
||||
if await page.locator('button:has-text("声明原创"):visible').count():
|
||||
await page.locator('button:has-text("声明原创"):visible').click()
|
||||
|
||||
async def main(self):
|
||||
async with async_playwright() as playwright:
|
||||
await self.upload(playwright)
|
||||
@@ -0,0 +1,5 @@
|
||||
from pathlib import Path
|
||||
|
||||
from sau_backend.conf import BASE_DIR
|
||||
|
||||
Path(BASE_DIR / "cookies" / "tk_uploader").mkdir(exist_ok=True)
|
||||
@@ -0,0 +1,265 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
from playwright.async_api import Playwright, async_playwright
|
||||
import os
|
||||
import asyncio
|
||||
from sau_backend.uploader.tk_uploader.tk_config import Tk_Locator
|
||||
from sau_backend.utils.base_social_media import set_init_script
|
||||
from sau_backend.utils.files_times import get_absolute_path
|
||||
from sau_backend.utils.log import tiktok_logger
|
||||
|
||||
|
||||
async def cookie_auth(account_file):
|
||||
async with async_playwright() as playwright:
|
||||
browser = await playwright.firefox.launch(headless=True)
|
||||
context = await browser.new_context(storage_state=account_file)
|
||||
context = await set_init_script(context)
|
||||
# 创建一个新的页面
|
||||
page = await context.new_page()
|
||||
# 访问指定的 URL
|
||||
await page.goto("https://www.tiktok.com/tiktokstudio/upload?lang=en")
|
||||
await page.wait_for_load_state('networkidle')
|
||||
try:
|
||||
# 选择所有的 select 元素
|
||||
select_elements = await page.query_selector_all('select')
|
||||
for element in select_elements:
|
||||
class_name = await element.get_attribute('class')
|
||||
# 使用正则表达式匹配特定模式的 class 名称
|
||||
if re.match(r'tiktok-.*-SelectFormContainer.*', class_name):
|
||||
tiktok_logger.error("[+] cookie expired")
|
||||
return False
|
||||
tiktok_logger.success("[+] cookie valid")
|
||||
return True
|
||||
except:
|
||||
tiktok_logger.success("[+] cookie valid")
|
||||
return True
|
||||
|
||||
|
||||
async def tiktok_setup(account_file, handle=False):
|
||||
account_file = get_absolute_path(account_file, "tk_uploader")
|
||||
if not os.path.exists(account_file) or not await cookie_auth(account_file):
|
||||
if not handle:
|
||||
return False
|
||||
tiktok_logger.info('[+] cookie file is not existed or expired. Now open the browser auto. Please login with your way(gmail phone, whatever, the cookie file will generated after login')
|
||||
await get_tiktok_cookie(account_file)
|
||||
return True
|
||||
|
||||
|
||||
async def get_tiktok_cookie(account_file):
|
||||
async with async_playwright() as playwright:
|
||||
options = {
|
||||
'args': [
|
||||
'--lang en-GB',
|
||||
],
|
||||
'headless': False, # Set headless option here
|
||||
}
|
||||
# Make sure to run headed.
|
||||
browser = await playwright.firefox.launch(**options)
|
||||
# Setup context however you like.
|
||||
context = await browser.new_context() # Pass any options
|
||||
context = await set_init_script(context)
|
||||
# Pause the page, and start recording manually.
|
||||
page = await context.new_page()
|
||||
await page.goto("https://www.tiktok.com/login?lang=en")
|
||||
await page.pause()
|
||||
# 点击调试器的继续,保存cookie
|
||||
await context.storage_state(path=account_file)
|
||||
|
||||
|
||||
class TiktokVideo(object):
|
||||
def __init__(self, title, file_path, tags, publish_date, account_file):
|
||||
self.title = title
|
||||
self.file_path = file_path
|
||||
self.tags = tags
|
||||
self.publish_date = publish_date
|
||||
self.account_file = account_file
|
||||
self.locator_base = None
|
||||
|
||||
|
||||
async def set_schedule_time(self, page, publish_date):
|
||||
schedule_input_element = self.locator_base.get_by_label('Schedule')
|
||||
await schedule_input_element.wait_for(state='visible') # 确保按钮可见
|
||||
|
||||
await schedule_input_element.click()
|
||||
scheduled_picker = self.locator_base.locator('div.scheduled-picker')
|
||||
await scheduled_picker.locator('div.TUXInputBox').nth(1).click()
|
||||
|
||||
calendar_month = await self.locator_base.locator('div.calendar-wrapper span.month-title').inner_text()
|
||||
|
||||
n_calendar_month = datetime.strptime(calendar_month, '%B').month
|
||||
|
||||
schedule_month = publish_date.month
|
||||
|
||||
if n_calendar_month != schedule_month:
|
||||
if n_calendar_month < schedule_month:
|
||||
arrow = self.locator_base.locator('div.calendar-wrapper span.arrow').nth(-1)
|
||||
else:
|
||||
arrow = self.locator_base.locator('div.calendar-wrapper span.arrow').nth(0)
|
||||
await arrow.click()
|
||||
|
||||
# day set
|
||||
valid_days_locator = self.locator_base.locator(
|
||||
'div.calendar-wrapper span.day.valid')
|
||||
valid_days = await valid_days_locator.count()
|
||||
for i in range(valid_days):
|
||||
day_element = valid_days_locator.nth(i)
|
||||
text = await day_element.inner_text()
|
||||
if text.strip() == str(publish_date.day):
|
||||
await day_element.click()
|
||||
break
|
||||
# time set
|
||||
await scheduled_picker.locator('div.TUXInputBox').nth(0).click()
|
||||
|
||||
hour_str = publish_date.strftime("%H")
|
||||
correct_minute = int(publish_date.minute / 5)
|
||||
minute_str = f"{correct_minute:02d}"
|
||||
|
||||
hour_selector = f"span.tiktok-timepicker-left:has-text('{hour_str}')"
|
||||
minute_selector = f"span.tiktok-timepicker-right:has-text('{minute_str}')"
|
||||
|
||||
# pick hour first
|
||||
await self.locator_base.locator(hour_selector).click()
|
||||
# click time button again
|
||||
# 等待某个特定的元素出现或状态变化,表明UI已更新
|
||||
await page.wait_for_timeout(1000) # 等待500毫秒
|
||||
await scheduled_picker.locator('div.TUXInputBox').nth(0).click()
|
||||
# pick minutes after
|
||||
await self.locator_base.locator(minute_selector).click()
|
||||
|
||||
# click title to remove the focus.
|
||||
await self.locator_base.locator("h1:has-text('Upload video')").click()
|
||||
|
||||
async def handle_upload_error(self, page):
|
||||
tiktok_logger.info("video upload error retrying.")
|
||||
select_file_button = self.locator_base.locator('button[aria-label="Select file"]')
|
||||
async with page.expect_file_chooser() as fc_info:
|
||||
await select_file_button.click()
|
||||
file_chooser = await fc_info.value
|
||||
await file_chooser.set_files(self.file_path)
|
||||
|
||||
async def upload(self, playwright: Playwright) -> None:
|
||||
browser = await playwright.firefox.launch(headless=False)
|
||||
context = await browser.new_context(storage_state=f"{self.account_file}")
|
||||
context = await set_init_script(context)
|
||||
page = await context.new_page()
|
||||
|
||||
await page.goto("https://www.tiktok.com/creator-center/upload")
|
||||
tiktok_logger.info(f'[+]Uploading-------{self.title}.mp4')
|
||||
|
||||
await page.wait_for_url("https://www.tiktok.com/tiktokstudio/upload", timeout=10000)
|
||||
|
||||
try:
|
||||
await page.wait_for_selector('iframe[data-tt="Upload_index_iframe"], div.upload-container', timeout=10000)
|
||||
tiktok_logger.info("Either iframe or div appeared.")
|
||||
except Exception as e:
|
||||
tiktok_logger.error("Neither iframe nor div appeared within the timeout.")
|
||||
|
||||
await self.choose_base_locator(page)
|
||||
|
||||
upload_button = self.locator_base.locator(
|
||||
'button:has-text("Select video"):visible')
|
||||
await upload_button.wait_for(state='visible') # 确保按钮可见
|
||||
|
||||
async with page.expect_file_chooser() as fc_info:
|
||||
await upload_button.click()
|
||||
file_chooser = await fc_info.value
|
||||
await file_chooser.set_files(self.file_path)
|
||||
|
||||
await self.add_title_tags(page)
|
||||
# detact upload status
|
||||
await self.detect_upload_status(page)
|
||||
if self.publish_date != 0:
|
||||
await self.set_schedule_time(page, self.publish_date)
|
||||
|
||||
await self.click_publish(page)
|
||||
|
||||
await context.storage_state(path=f"{self.account_file}") # save cookie
|
||||
tiktok_logger.info(' [-] update cookie!')
|
||||
await asyncio.sleep(2) # close delay for look the video status
|
||||
# close all
|
||||
await context.close()
|
||||
await browser.close()
|
||||
|
||||
async def add_title_tags(self, page):
|
||||
|
||||
editor_locator = self.locator_base.locator('div.public-DraftEditor-content')
|
||||
await editor_locator.click()
|
||||
|
||||
await page.keyboard.press("End")
|
||||
|
||||
await page.keyboard.press("Control+A")
|
||||
|
||||
await page.keyboard.press("Delete")
|
||||
|
||||
await page.keyboard.press("End")
|
||||
|
||||
await page.wait_for_timeout(1000) # 等待1秒
|
||||
|
||||
await page.keyboard.insert_text(self.title)
|
||||
await page.wait_for_timeout(1000) # 等待1秒
|
||||
await page.keyboard.press("End")
|
||||
|
||||
await page.keyboard.press("Enter")
|
||||
|
||||
# tag part
|
||||
for index, tag in enumerate(self.tags, start=1):
|
||||
tiktok_logger.info("Setting the %s tag" % index)
|
||||
await page.keyboard.press("End")
|
||||
await page.wait_for_timeout(1000) # 等待1秒
|
||||
await page.keyboard.insert_text("#" + tag + " ")
|
||||
await page.keyboard.press("Space")
|
||||
await page.wait_for_timeout(1000) # 等待1秒
|
||||
|
||||
await page.keyboard.press("Backspace")
|
||||
await page.keyboard.press("End")
|
||||
|
||||
async def click_publish(self, page):
|
||||
success_flag_div = '#\\:r9\\:'
|
||||
while True:
|
||||
try:
|
||||
publish_button = self.locator_base.locator('div.btn-post')
|
||||
if await publish_button.count():
|
||||
await publish_button.click()
|
||||
|
||||
await self.locator_base.locator(success_flag_div).wait_for(state="visible", timeout=3000)
|
||||
tiktok_logger.success(" [-] video published success")
|
||||
break
|
||||
except Exception as e:
|
||||
if await self.locator_base.locator(success_flag_div).count():
|
||||
tiktok_logger.success(" [-]video published success")
|
||||
break
|
||||
else:
|
||||
tiktok_logger.exception(f" [-] Exception: {e}")
|
||||
tiktok_logger.info(" [-] video publishing")
|
||||
await page.screenshot(full_page=True)
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
async def detect_upload_status(self, page):
|
||||
while True:
|
||||
try:
|
||||
if await self.locator_base.locator('div.btn-post > button').get_attribute("disabled") is None:
|
||||
tiktok_logger.info(" [-]video uploaded.")
|
||||
break
|
||||
else:
|
||||
tiktok_logger.info(" [-] video uploading...")
|
||||
await asyncio.sleep(2)
|
||||
if await self.locator_base.locator('button[aria-label="Select file"]').count():
|
||||
tiktok_logger.info(" [-] found some error while uploading now retry...")
|
||||
await self.handle_upload_error(page)
|
||||
except:
|
||||
tiktok_logger.info(" [-] video uploading...")
|
||||
await asyncio.sleep(2)
|
||||
|
||||
async def choose_base_locator(self, page):
|
||||
# await page.wait_for_selector('div.upload-container')
|
||||
if await page.locator('iframe[data-tt="Upload_index_iframe"]').count():
|
||||
self.locator_base = self.locator_base
|
||||
else:
|
||||
self.locator_base = page.locator(Tk_Locator.default)
|
||||
|
||||
async def main(self):
|
||||
async with async_playwright() as playwright:
|
||||
await self.upload(playwright)
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
from playwright.async_api import Playwright, async_playwright
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
from sau_backend.conf import LOCAL_CHROME_PATH
|
||||
from sau_backend.uploader.tk_uploader.tk_config import Tk_Locator
|
||||
from sau_backend.utils.base_social_media import set_init_script
|
||||
from sau_backend.utils.files_times import get_absolute_path
|
||||
from sau_backend.utils.log import tiktok_logger
|
||||
|
||||
|
||||
async def cookie_auth(account_file):
|
||||
async with async_playwright() as playwright:
|
||||
browser = await playwright.chromium.launch(headless=True)
|
||||
context = await browser.new_context(storage_state=account_file)
|
||||
context = await set_init_script(context)
|
||||
# 创建一个新的页面
|
||||
page = await context.new_page()
|
||||
# 访问指定的 URL
|
||||
await page.goto("https://www.tiktok.com/tiktokstudio/upload?lang=en")
|
||||
await page.wait_for_load_state('networkidle')
|
||||
try:
|
||||
# 选择所有的 select 元素
|
||||
select_elements = await page.query_selector_all('select')
|
||||
for element in select_elements:
|
||||
class_name = await element.get_attribute('class')
|
||||
# 使用正则表达式匹配特定模式的 class 名称
|
||||
if re.match(r'tiktok-.*-SelectFormContainer.*', class_name):
|
||||
tiktok_logger.error("[+] cookie expired")
|
||||
return False
|
||||
tiktok_logger.success("[+] cookie valid")
|
||||
return True
|
||||
except:
|
||||
tiktok_logger.success("[+] cookie valid")
|
||||
return True
|
||||
|
||||
|
||||
async def tiktok_setup(account_file, handle=False):
|
||||
account_file = get_absolute_path(account_file, "tk_uploader")
|
||||
if not os.path.exists(account_file) or not await cookie_auth(account_file):
|
||||
if not handle:
|
||||
return False
|
||||
tiktok_logger.info('[+] cookie file is not existed or expired. Now open the browser auto. Please login with your way(gmail phone, whatever, the cookie file will generated after login')
|
||||
await get_tiktok_cookie(account_file)
|
||||
return True
|
||||
|
||||
|
||||
async def get_tiktok_cookie(account_file):
|
||||
async with async_playwright() as playwright:
|
||||
options = {
|
||||
'args': [
|
||||
'--lang en-GB',
|
||||
],
|
||||
'headless': False, # Set headless option here
|
||||
}
|
||||
# Make sure to run headed.
|
||||
browser = await playwright.chromium.launch(**options)
|
||||
# Setup context however you like.
|
||||
context = await browser.new_context() # Pass any options
|
||||
context = await set_init_script(context)
|
||||
# Pause the page, and start recording manually.
|
||||
page = await context.new_page()
|
||||
await page.goto("https://www.tiktok.com/login?lang=en")
|
||||
await page.pause()
|
||||
# 点击调试器的继续,保存cookie
|
||||
await context.storage_state(path=account_file)
|
||||
|
||||
|
||||
class TiktokVideo(object):
|
||||
def __init__(self, title, file_path, tags, publish_date, account_file, thumbnail_path=None):
|
||||
self.title = title
|
||||
self.file_path = file_path
|
||||
self.tags = tags
|
||||
self.publish_date = publish_date
|
||||
self.thumbnail_path = thumbnail_path
|
||||
self.account_file = account_file
|
||||
self.local_executable_path = LOCAL_CHROME_PATH
|
||||
self.locator_base = None
|
||||
|
||||
async def set_schedule_time(self, page, publish_date):
|
||||
schedule_input_element = self.locator_base.get_by_label('Schedule')
|
||||
await schedule_input_element.wait_for(state='visible') # 确保按钮可见
|
||||
|
||||
await schedule_input_element.click(force=True)
|
||||
if await self.locator_base.locator('div.TUXButton-content >> text=Allow').count():
|
||||
await self.locator_base.locator('div.TUXButton-content >> text=Allow').click()
|
||||
|
||||
scheduled_picker = self.locator_base.locator('div.scheduled-picker')
|
||||
await scheduled_picker.locator('div.TUXInputBox').nth(1).click()
|
||||
|
||||
calendar_month = await self.locator_base.locator(
|
||||
'div.calendar-wrapper span.month-title').inner_text()
|
||||
|
||||
n_calendar_month = datetime.strptime(calendar_month, '%B').month
|
||||
|
||||
schedule_month = publish_date.month
|
||||
|
||||
if n_calendar_month != schedule_month:
|
||||
if n_calendar_month < schedule_month:
|
||||
arrow = self.locator_base.locator('div.calendar-wrapper span.arrow').nth(-1)
|
||||
else:
|
||||
arrow = self.locator_base.locator('div.calendar-wrapper span.arrow').nth(0)
|
||||
await arrow.click()
|
||||
|
||||
# day set
|
||||
valid_days_locator = self.locator_base.locator(
|
||||
'div.calendar-wrapper span.day.valid')
|
||||
valid_days = await valid_days_locator.count()
|
||||
for i in range(valid_days):
|
||||
day_element = valid_days_locator.nth(i)
|
||||
text = await day_element.inner_text()
|
||||
if text.strip() == str(publish_date.day):
|
||||
await day_element.click()
|
||||
break
|
||||
# time set
|
||||
await scheduled_picker.locator('div.TUXInputBox').nth(0).click()
|
||||
|
||||
hour_str = publish_date.strftime("%H")
|
||||
correct_minute = int(publish_date.minute / 5)
|
||||
minute_str = f"{correct_minute:02d}"
|
||||
|
||||
hour_selector = f"span.tiktok-timepicker-left:has-text('{hour_str}')"
|
||||
minute_selector = f"span.tiktok-timepicker-right:has-text('{minute_str}')"
|
||||
|
||||
# pick hour first
|
||||
await page.wait_for_timeout(1000) # 等待500毫秒
|
||||
await self.locator_base.locator(hour_selector).click()
|
||||
# click time button again
|
||||
await page.wait_for_timeout(1000) # 等待500毫秒
|
||||
# pick minutes after
|
||||
await self.locator_base.locator(minute_selector).click()
|
||||
|
||||
# click title to remove the focus.
|
||||
# await self.locator_base.locator("h1:has-text('Upload video')").click()
|
||||
|
||||
async def handle_upload_error(self, page):
|
||||
tiktok_logger.info("video upload error retrying.")
|
||||
select_file_button = self.locator_base.locator('button[aria-label="Select file"]')
|
||||
async with page.expect_file_chooser() as fc_info:
|
||||
await select_file_button.click()
|
||||
file_chooser = await fc_info.value
|
||||
await file_chooser.set_files(self.file_path)
|
||||
|
||||
async def upload(self, playwright: Playwright) -> None:
|
||||
browser = await playwright.chromium.launch(headless=False, executable_path=self.local_executable_path)
|
||||
context = await browser.new_context(storage_state=f"{self.account_file}")
|
||||
context = await set_init_script(context)
|
||||
page = await context.new_page()
|
||||
|
||||
# change language to eng first
|
||||
await self.change_language(page)
|
||||
await page.goto("https://www.tiktok.com/tiktokstudio/upload")
|
||||
tiktok_logger.info(f'[+]Uploading-------{self.title}.mp4')
|
||||
|
||||
await page.wait_for_url("https://www.tiktok.com/tiktokstudio/upload", timeout=10000)
|
||||
|
||||
try:
|
||||
await page.wait_for_selector('iframe[data-tt="Upload_index_iframe"], div.upload-container', timeout=10000)
|
||||
tiktok_logger.info("Either iframe or div appeared.")
|
||||
except Exception as e:
|
||||
tiktok_logger.error("Neither iframe nor div appeared within the timeout.")
|
||||
|
||||
await self.choose_base_locator(page)
|
||||
|
||||
upload_button = self.locator_base.locator(
|
||||
'button:has-text("Select video"):visible')
|
||||
await upload_button.wait_for(state='visible') # 确保按钮可见
|
||||
|
||||
async with page.expect_file_chooser() as fc_info:
|
||||
await upload_button.click()
|
||||
file_chooser = await fc_info.value
|
||||
await file_chooser.set_files(self.file_path)
|
||||
|
||||
await self.add_title_tags(page)
|
||||
# detect upload status
|
||||
await self.detect_upload_status(page)
|
||||
if self.thumbnail_path:
|
||||
tiktok_logger.info(f'[+] Uploading thumbnail file {self.title}.png')
|
||||
await self.upload_thumbnails(page)
|
||||
|
||||
if self.publish_date != 0:
|
||||
await self.set_schedule_time(page, self.publish_date)
|
||||
|
||||
await self.click_publish(page)
|
||||
|
||||
await context.storage_state(path=f"{self.account_file}") # save cookie
|
||||
tiktok_logger.info(' [-] update cookie!')
|
||||
await asyncio.sleep(2) # close delay for look the video status
|
||||
# close all
|
||||
await context.close()
|
||||
await browser.close()
|
||||
|
||||
async def add_title_tags(self, page):
|
||||
|
||||
editor_locator = self.locator_base.locator('div.public-DraftEditor-content')
|
||||
await editor_locator.click()
|
||||
|
||||
await page.keyboard.press("End")
|
||||
|
||||
await page.keyboard.press("Control+A")
|
||||
|
||||
await page.keyboard.press("Delete")
|
||||
|
||||
await page.keyboard.press("End")
|
||||
|
||||
await page.wait_for_timeout(1000) # 等待1秒
|
||||
|
||||
await page.keyboard.insert_text(self.title)
|
||||
await page.wait_for_timeout(1000) # 等待1秒
|
||||
await page.keyboard.press("End")
|
||||
|
||||
await page.keyboard.press("Enter")
|
||||
|
||||
# tag part
|
||||
for index, tag in enumerate(self.tags, start=1):
|
||||
tiktok_logger.info("Setting the %s tag" % index)
|
||||
await page.keyboard.press("End")
|
||||
await page.wait_for_timeout(1000) # 等待1秒
|
||||
await page.keyboard.insert_text("#" + tag + " ")
|
||||
await page.keyboard.press("Space")
|
||||
await page.wait_for_timeout(1000) # 等待1秒
|
||||
|
||||
await page.keyboard.press("Backspace")
|
||||
await page.keyboard.press("End")
|
||||
|
||||
async def upload_thumbnails(self, page):
|
||||
await self.locator_base.locator(".cover-container").click()
|
||||
await self.locator_base.locator(".cover-edit-container >> text=Upload cover").click()
|
||||
async with page.expect_file_chooser() as fc_info:
|
||||
await self.locator_base.locator(".upload-image-upload-area").click()
|
||||
file_chooser = await fc_info.value
|
||||
await file_chooser.set_files(self.thumbnail_path)
|
||||
await self.locator_base.locator('div.cover-edit-panel:not(.hide-panel)').get_by_role(
|
||||
"button", name="Confirm").click()
|
||||
await page.wait_for_timeout(3000) # wait 3s, fix it later
|
||||
|
||||
async def change_language(self, page):
|
||||
# set the language to english
|
||||
await page.goto("https://www.tiktok.com")
|
||||
await page.wait_for_load_state('domcontentloaded')
|
||||
await page.wait_for_selector('[data-e2e="nav-more-menu"]')
|
||||
# 已经设置为英文, 省略这个步骤
|
||||
if await page.locator('[data-e2e="nav-more-menu"]').text_content() == "More":
|
||||
return
|
||||
|
||||
await page.locator('[data-e2e="nav-more-menu"]').click()
|
||||
await page.locator('[data-e2e="language-select"]').click()
|
||||
await page.locator('#creator-tools-selection-menu-header >> text=English').click()
|
||||
|
||||
async def click_publish(self, page):
|
||||
success_flag_div = 'div.common-modal-confirm-modal'
|
||||
while True:
|
||||
try:
|
||||
publish_button = self.locator_base.locator('div.button-group button').nth(0)
|
||||
if await publish_button.count():
|
||||
await publish_button.click()
|
||||
|
||||
await page.wait_for_url("https://www.tiktok.com/tiktokstudio/content", timeout=3000)
|
||||
tiktok_logger.success(" [-] video published success")
|
||||
break
|
||||
except Exception as e:
|
||||
tiktok_logger.exception(f" [-] Exception: {e}")
|
||||
tiktok_logger.info(" [-] video publishing")
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
async def detect_upload_status(self, page):
|
||||
while True:
|
||||
try:
|
||||
# if await self.locator_base.locator('div.btn-post > button').get_attribute("disabled") is None:
|
||||
if await self.locator_base.locator(
|
||||
'div.button-group > button >> text=Post').get_attribute("disabled") is None:
|
||||
tiktok_logger.info(" [-]video uploaded.")
|
||||
break
|
||||
else:
|
||||
tiktok_logger.info(" [-] video uploading...")
|
||||
await asyncio.sleep(2)
|
||||
if await self.locator_base.locator(
|
||||
'button[aria-label="Select file"]').count():
|
||||
tiktok_logger.info(" [-] found some error while uploading now retry...")
|
||||
await self.handle_upload_error(page)
|
||||
except:
|
||||
tiktok_logger.info(" [-] video uploading...")
|
||||
await asyncio.sleep(2)
|
||||
|
||||
async def choose_base_locator(self, page):
|
||||
# await page.wait_for_selector('div.upload-container')
|
||||
if await page.locator('iframe[data-tt="Upload_index_iframe"]').count():
|
||||
self.locator_base = page.frame_locator(Tk_Locator.tk_iframe)
|
||||
else:
|
||||
self.locator_base = page.locator(Tk_Locator.default)
|
||||
|
||||
async def main(self):
|
||||
async with async_playwright() as playwright:
|
||||
await self.upload(playwright)
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
class Tk_Locator(object):
|
||||
tk_iframe = '[data-tt="Upload_index_iframe"]'
|
||||
default = 'body'
|
||||
@@ -0,0 +1,2 @@
|
||||
[account1]
|
||||
cookies = changeme
|
||||
@@ -0,0 +1,58 @@
|
||||
import configparser
|
||||
import json
|
||||
import pathlib
|
||||
from time import sleep
|
||||
|
||||
import requests
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
from sau_backend.conf import BASE_DIR, XHS_SERVER
|
||||
|
||||
config = configparser.RawConfigParser()
|
||||
config.read('accounts.ini')
|
||||
|
||||
|
||||
def sign_local(uri, data=None, a1="", web_session=""):
|
||||
for _ in range(10):
|
||||
try:
|
||||
with sync_playwright() as playwright:
|
||||
stealth_js_path = pathlib.Path(BASE_DIR / "utils/stealth.min.js")
|
||||
chromium = playwright.chromium
|
||||
|
||||
# 如果一直失败可尝试设置成 False 让其打开浏览器,适当添加 sleep 可查看浏览器状态
|
||||
browser = chromium.launch(headless=True)
|
||||
|
||||
browser_context = browser.new_context()
|
||||
browser_context.add_init_script(path=stealth_js_path)
|
||||
context_page = browser_context.new_page()
|
||||
context_page.goto("https://www.xiaohongshu.com")
|
||||
browser_context.add_cookies([
|
||||
{'name': 'a1', 'value': a1, 'domain': ".xiaohongshu.com", 'path': "/"}]
|
||||
)
|
||||
context_page.reload()
|
||||
# 这个地方设置完浏览器 cookie 之后,如果这儿不 sleep 一下签名获取就失败了,如果经常失败请设置长一点试试
|
||||
sleep(2)
|
||||
encrypt_params = context_page.evaluate("([url, data]) => window._webmsxyw(url, data)", [uri, data])
|
||||
return {
|
||||
"x-s": encrypt_params["X-s"],
|
||||
"x-t": str(encrypt_params["X-t"])
|
||||
}
|
||||
except Exception:
|
||||
# 这儿有时会出现 window._webmsxyw is not a function 或未知跳转错误,因此加一个失败重试趴
|
||||
pass
|
||||
raise Exception("重试了这么多次还是无法签名成功,寄寄寄")
|
||||
|
||||
|
||||
def sign(uri, data=None, a1="", web_session=""):
|
||||
# 填写自己的 flask 签名服务端口地址
|
||||
res = requests.post(f"{XHS_SERVER}/sign",
|
||||
json={"uri": uri, "data": data, "a1": a1, "web_session": web_session})
|
||||
signs = res.json()
|
||||
return {
|
||||
"x-s": signs["x-s"],
|
||||
"x-t": signs["x-t"]
|
||||
}
|
||||
|
||||
|
||||
def beauty_print(data: dict):
|
||||
print(json.dumps(data, ensure_ascii=False, indent=2))
|
||||
@@ -0,0 +1,34 @@
|
||||
import datetime
|
||||
import json
|
||||
import qrcode
|
||||
from time import sleep
|
||||
|
||||
from xhs import XhsClient
|
||||
|
||||
from sau_backend.uploader.xhs_uploader.main import sign
|
||||
|
||||
# pip install qrcode
|
||||
if __name__ == '__main__':
|
||||
xhs_client = XhsClient(sign=sign, timeout=60)
|
||||
print(datetime.datetime.now())
|
||||
qr_res = xhs_client.get_qrcode()
|
||||
qr_id = qr_res["qr_id"]
|
||||
qr_code = qr_res["code"]
|
||||
|
||||
qr = qrcode.QRCode(version=1, error_correction=qrcode.ERROR_CORRECT_L,
|
||||
box_size=50,
|
||||
border=1)
|
||||
qr.add_data(qr_res["url"])
|
||||
qr.make()
|
||||
qr.print_ascii()
|
||||
|
||||
while True:
|
||||
check_qrcode = xhs_client.check_qrcode(qr_id, qr_code)
|
||||
print(check_qrcode)
|
||||
sleep(1)
|
||||
if check_qrcode["code_status"] == 2:
|
||||
print(json.dumps(check_qrcode["login_info"], indent=4))
|
||||
print("当前 cookie:" + xhs_client.cookie)
|
||||
break
|
||||
|
||||
print(json.dumps(xhs_client.get_self_info(), indent=4))
|
||||
@@ -0,0 +1,24 @@
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from sau_backend.conf import BASE_DIR
|
||||
|
||||
SOCIAL_MEDIA_DOUYIN = "douyin"
|
||||
SOCIAL_MEDIA_TENCENT = "tencent"
|
||||
SOCIAL_MEDIA_TIKTOK = "tiktok"
|
||||
SOCIAL_MEDIA_BILIBILI = "bilibili"
|
||||
SOCIAL_MEDIA_KUAISHOU = "kuaishou"
|
||||
|
||||
|
||||
def get_supported_social_media() -> List[str]:
|
||||
return [SOCIAL_MEDIA_DOUYIN, SOCIAL_MEDIA_TENCENT, SOCIAL_MEDIA_TIKTOK, SOCIAL_MEDIA_KUAISHOU]
|
||||
|
||||
|
||||
def get_cli_action() -> List[str]:
|
||||
return ["upload", "login", "watch"]
|
||||
|
||||
|
||||
async def set_init_script(context):
|
||||
stealth_js_path = Path(BASE_DIR / "utils/stealth.min.js")
|
||||
await context.add_init_script(path=stealth_js_path)
|
||||
return context
|
||||
@@ -0,0 +1,309 @@
|
||||
import enum
|
||||
|
||||
|
||||
class TencentZoneTypes(enum.Enum):
|
||||
LIFESTYLE = '生活'
|
||||
CUTE_KIDS = '萌娃'
|
||||
MUSIC = '音乐'
|
||||
KNOWLEDGE = '知识'
|
||||
EMOTION = '情感'
|
||||
TRAVEL_SCENERY = '旅行风景'
|
||||
FASHION = '时尚'
|
||||
FOOD = '美食'
|
||||
LIFE_HACKS = '生活技巧'
|
||||
DANCE = '舞蹈'
|
||||
MOVIES_TV_SHOWS = '影视综艺'
|
||||
SPORTS = '运动'
|
||||
FUNNY = '搞笑'
|
||||
CELEBRITIES = '明星名人'
|
||||
NEWS_INFO = '新闻资讯'
|
||||
GAMING = '游戏'
|
||||
AUTOMOTIVE = '车'
|
||||
ANIME = '二次元'
|
||||
TALENT = '才艺'
|
||||
CUTE_PETS = '萌宠'
|
||||
INDUSTRY_MACHINERY_CONSTRUCTION = '机械'
|
||||
ANIMALS = '动物'
|
||||
PARENTING = '育儿'
|
||||
TECHNOLOGY = '科技'
|
||||
|
||||
class VideoZoneTypes(enum.Enum):
|
||||
"""
|
||||
所有分区枚举
|
||||
|
||||
- MAINPAGE: 主页
|
||||
- ANIME: 番剧
|
||||
- ANIME_SERIAL: 连载中番剧
|
||||
- ANIME_FINISH: 已完结番剧
|
||||
- ANIME_INFORMATION: 资讯
|
||||
- ANIME_OFFICAL: 官方延伸
|
||||
- MOVIE: 电影
|
||||
- GUOCHUANG: 国创
|
||||
- GUOCHUANG_CHINESE: 国产动画
|
||||
- GUOCHUANG_ORIGINAL: 国产原创相关
|
||||
- GUOCHUANG_PUPPETRY: 布袋戏
|
||||
- GUOCHUANG_MOTIONCOMIC: 动态漫·广播剧
|
||||
- GUOCHUANG_INFORMATION: 资讯
|
||||
- TELEPLAY: 电视剧
|
||||
- DOCUMENTARY: 纪录片
|
||||
- DOUGA: 动画
|
||||
- DOUGA_MAD: MAD·AMV
|
||||
- DOUGA_MMD: MMD·3D
|
||||
- DOUGA_VOICE: 短片·手书·配音
|
||||
- DOUGA_GARAGE_KIT: 手办·模玩
|
||||
- DOUGA_TOKUSATSU: 特摄
|
||||
- DOUGA_ACGNTALKS: 动漫杂谈
|
||||
- DOUGA_OTHER: 综合
|
||||
- GAME: 游戏
|
||||
- GAME_STAND_ALONE: 单机游戏
|
||||
- GAME_ESPORTS: 电子竞技
|
||||
- GAME_MOBILE: 手机游戏
|
||||
- GAME_ONLINE: 网络游戏
|
||||
- GAME_BOARD: 桌游棋牌
|
||||
- GAME_GMV: GMV
|
||||
- GAME_MUSIC: 音游
|
||||
- GAME_MUGEN: Mugen
|
||||
- KICHIKU: 鬼畜
|
||||
- KICHIKU_GUIDE: 鬼畜调教
|
||||
- KICHIKU_MAD: 音MAD
|
||||
- KICHIKU_MANUAL_VOCALOID: 人力VOCALOID
|
||||
- KICHIKU_THEATRE: 鬼畜剧场
|
||||
- KICHIKU_COURSE: 教程演示
|
||||
- MUSIC: 音乐
|
||||
- MUSIC_ORIGINAL: 原创音乐
|
||||
- MUSIC_COVER: 翻唱
|
||||
- MUSIC_PERFORM: 演奏
|
||||
- MUSIC_VOCALOID: VOCALOID·UTAU
|
||||
- MUSIC_LIVE: 音乐现场
|
||||
- MUSIC_MV: MV
|
||||
- MUSIC_COMMENTARY: 乐评盘点
|
||||
- MUSIC_TUTORIAL: 音乐教学
|
||||
- MUSIC_OTHER: 音乐综合
|
||||
- DANCE: 舞蹈
|
||||
- DANCE_OTAKU: 宅舞
|
||||
- DANCE_HIPHOP: 街舞
|
||||
- DANCE_STAR: 明星舞蹈
|
||||
- DANCE_CHINA: 中国舞
|
||||
- DANCE_THREE_D: 舞蹈综合
|
||||
- DANCE_DEMO: 舞蹈教程
|
||||
- CINEPHILE: 影视
|
||||
- CINEPHILE_CINECISM: 影视杂谈
|
||||
- CINEPHILE_MONTAGE: 影视剪辑
|
||||
- CINEPHILE_SHORTFILM: 小剧场
|
||||
- CINEPHILE_TRAILER_INFO: 预告·资讯
|
||||
- ENT: 娱乐
|
||||
- ENT_VARIETY: 综艺
|
||||
- ENT_TALKER: 娱乐杂谈
|
||||
- ENT_FANS: 粉丝创作
|
||||
- ENT_CELEBRITY: 明星综合
|
||||
- KNOWLEDGE: 知识
|
||||
- KNOWLEDGE_SCIENCE: 科学科普
|
||||
- KNOWLEDGE_SOCIAL_SCIENCE: 社科·法律·心理
|
||||
- KNOWLEDGE_HUMANITY_HISTORY: 人文历史
|
||||
- KNOWLEDGE_BUSINESS: 财经商业
|
||||
- KNOWLEDGE_CAMPUS: 校园学习
|
||||
- KNOWLEDGE_CAREER: 职业职场
|
||||
- KNOWLEDGE_DESIGN: 设计·创意
|
||||
- KNOWLEDGE_SKILL: 野生技能协会
|
||||
- TECH: 科技
|
||||
- TECH_DIGITAL: 数码
|
||||
- TECH_APPLICATION: 软件应用
|
||||
- TECH_COMPUTER_TECH: 计算机技术
|
||||
- TECH_INDUSTRY: 科工机械
|
||||
- INFORMATION: 资讯
|
||||
- INFORMATION_HOTSPOT: 热点
|
||||
- INFORMATION_GLOBAL: 环球
|
||||
- INFORMATION_SOCIAL: 社会
|
||||
- INFORMATION_MULTIPLE: 综合
|
||||
- FOOD: 美食
|
||||
- FOOD_MAKE: 美食制作
|
||||
- FOOD_DETECTIVE: 美食侦探
|
||||
- FOOD_MEASUREMENT: 美食测评
|
||||
- FOOD_RURAL: 田园美食
|
||||
- FOOD_RECORD: 美食记录
|
||||
- LIFE: 生活
|
||||
- LIFE_FUNNY: 搞笑
|
||||
- LIFE_TRAVEL: 出行
|
||||
- LIFE_RURALLIFE: 三农
|
||||
- LIFE_HOME: 家居房产
|
||||
- LIFE_HANDMAKE: 手工
|
||||
- LIFE_PAINTING: 绘画
|
||||
- LIFE_DAILY: 日常
|
||||
- CAR: 汽车
|
||||
- CAR_RACING: 赛车
|
||||
- CAR_MODIFIEDVEHICLE: 改装玩车
|
||||
- CAR_NEWENERGYVEHICLE: 新能源车
|
||||
- CAR_TOURINGCAR: 房车
|
||||
- CAR_MOTORCYCLE: 摩托车
|
||||
- CAR_STRATEGY: 购车攻略
|
||||
- CAR_LIFE: 汽车生活
|
||||
- FASHION: 时尚
|
||||
- FASHION_MAKEUP: 美妆护肤
|
||||
- FASHION_COS: 仿妆cos
|
||||
- FASHION_CLOTHING: 穿搭
|
||||
- FASHION_TREND: 时尚潮流
|
||||
- SPORTS: 运动
|
||||
- SPORTS_BASKETBALL: 篮球
|
||||
- SPORTS_FOOTBALL: 足球
|
||||
- SPORTS_AEROBICS: 健身
|
||||
- SPORTS_ATHLETIC: 竞技体育
|
||||
- SPORTS_CULTURE: 运动文化
|
||||
- SPORTS_COMPREHENSIVE: 运动综合
|
||||
- ANIMAL: 动物圈
|
||||
- ANIMAL_CAT: 喵星人
|
||||
- ANIMAL_DOG: 汪星人
|
||||
- ANIMAL_PANDA: 大熊猫
|
||||
- ANIMAL_WILD_ANIMAL: 野生动物
|
||||
- ANIMAL_REPTILES: 爬宠
|
||||
- ANIMAL_COMPOSITE: 动物综合
|
||||
- VLOG: VLOG
|
||||
"""
|
||||
|
||||
MAINPAGE = 0
|
||||
|
||||
ANIME = 13
|
||||
ANIME_SERIAL = 33
|
||||
ANIME_FINISH = 32
|
||||
ANIME_INFORMATION = 51
|
||||
ANIME_OFFICAL = 152
|
||||
|
||||
MOVIE = 23
|
||||
|
||||
GUOCHUANG = 167
|
||||
GUOCHUANG_CHINESE = 153
|
||||
GUOCHUANG_ORIGINAL = 168
|
||||
GUOCHUANG_PUPPETRY = 169
|
||||
GUOCHUANG_MOTIONCOMIC = 195
|
||||
GUOCHUANG_INFORMATION = 170
|
||||
|
||||
TELEPLAY = 11
|
||||
|
||||
DOCUMENTARY = 177
|
||||
|
||||
DOUGA = 1
|
||||
DOUGA_MAD = 24
|
||||
DOUGA_MMD = 25
|
||||
DOUGA_VOICE = 47
|
||||
DOUGA_GARAGE_KIT = 210
|
||||
DOUGA_TOKUSATSU = 86
|
||||
DOUGA_ACGNTALKS = 253
|
||||
DOUGA_OTHER = 27
|
||||
|
||||
GAME = 4
|
||||
GAME_STAND_ALONE = 17
|
||||
GAME_ESPORTS = 171
|
||||
GAME_MOBILE = 172
|
||||
GAME_ONLINE = 65
|
||||
GAME_BOARD = 173
|
||||
GAME_GMV = 121
|
||||
GAME_MUSIC = 136
|
||||
GAME_MUGEN = 19
|
||||
|
||||
KICHIKU = 119
|
||||
KICHIKU_GUIDE = 22
|
||||
KICHIKU_MAD = 26
|
||||
KICHIKU_MANUAL_VOCALOID = 126
|
||||
KICHIKU_THEATRE = 216
|
||||
KICHIKU_COURSE = 127
|
||||
|
||||
MUSIC = 3
|
||||
MUSIC_ORIGINAL = 28
|
||||
MUSIC_COVER = 31
|
||||
MUSIC_PERFORM = 59
|
||||
MUSIC_VOCALOID = 30
|
||||
MUSIC_LIVE = 29
|
||||
MUSIC_MV = 193
|
||||
MUSIC_COMMENTARY = 243
|
||||
MUSIC_TUTORIAL = 244
|
||||
MUSIC_OTHER = 130
|
||||
|
||||
DANCE = 129
|
||||
DANCE_OTAKU = 20
|
||||
DANCE_HIPHOP = 198
|
||||
DANCE_STAR = 199
|
||||
DANCE_CHINA = 200
|
||||
DANCE_THREE_D = 154
|
||||
DANCE_DEMO = 156
|
||||
|
||||
CINEPHILE = 181
|
||||
CINEPHILE_CINECISM = 182
|
||||
CINEPHILE_MONTAGE = 183
|
||||
CINEPHILE_SHORTFILM = 85
|
||||
CINEPHILE_TRAILER_INFO = 184
|
||||
|
||||
ENT = 5
|
||||
ENT_VARIETY = 71
|
||||
ENT_TALKER = 241
|
||||
ENT_FANS = 242
|
||||
ENT_CELEBRITY = 137
|
||||
|
||||
KNOWLEDGE = 36
|
||||
KNOWLEDGE_SCIENCE = 201
|
||||
KNOWLEDGE_SOCIAL_SCIENCE = 124
|
||||
KNOWLEDGE_HUMANITY_HISTORY = 228
|
||||
KNOWLEDGE_BUSINESS = 207
|
||||
KNOWLEDGE_CAMPUS = 208
|
||||
KNOWLEDGE_CAREER = 209
|
||||
KNOWLEDGE_DESIGN = 229
|
||||
KNOWLEDGE_SKILL = 122
|
||||
|
||||
TECH = 188
|
||||
TECH_DIGITAL = 95
|
||||
TECH_APPLICATION = 230
|
||||
TECH_COMPUTER_TECH = 231
|
||||
TECH_INDUSTRY = 232
|
||||
|
||||
INFORMATION = 202
|
||||
INFORMATION_HOTSPOT = 203
|
||||
INFORMATION_GLOBAL = 204
|
||||
INFORMATION_SOCIAL = 205
|
||||
INFORMATION_MULTIPLE = 206
|
||||
|
||||
FOOD = 211
|
||||
FOOD_MAKE = 76
|
||||
FOOD_DETECTIVE = 212
|
||||
FOOD_MEASUREMENT = 213
|
||||
FOOD_RURAL = 214
|
||||
FOOD_RECORD = 215
|
||||
|
||||
LIFE = 160
|
||||
LIFE_FUNNY = 138
|
||||
LIFE_TRAVEL = 250
|
||||
LIFE_RURALLIFE = 251
|
||||
LIFE_HOME = 239
|
||||
LIFE_HANDMAKE = 161
|
||||
LIFE_PAINTING = 162
|
||||
LIFE_DAILY = 21
|
||||
|
||||
CAR = 223
|
||||
CAR_RACING = 245
|
||||
CAR_MODIFIEDVEHICLE = 246
|
||||
CAR_NEWENERGYVEHICLE = 247
|
||||
CAR_TOURINGCAR = 248
|
||||
CAR_MOTORCYCLE = 240
|
||||
CAR_STRATEGY = 227
|
||||
CAR_LIFE = 176
|
||||
|
||||
FASHION = 155
|
||||
FASHION_MAKEUP = 157
|
||||
FASHION_COS = 252
|
||||
FASHION_CLOTHING = 158
|
||||
FASHION_TREND = 159
|
||||
|
||||
SPORTS = 234
|
||||
SPORTS_BASKETBALL = 235
|
||||
SPORTS_FOOTBALL = 249
|
||||
SPORTS_AEROBICS = 164
|
||||
SPORTS_ATHLETIC = 236
|
||||
SPORTS_CULTURE = 237
|
||||
SPORTS_COMPREHENSIVE = 238
|
||||
|
||||
ANIMAL = 217
|
||||
ANIMAL_CAT = 218
|
||||
ANIMAL_DOG = 219
|
||||
ANIMAL_PANDA = 220
|
||||
ANIMAL_WILD_ANIMAL = 221
|
||||
ANIMAL_REPTILES = 222
|
||||
ANIMAL_COMPOSITE = 75
|
||||
|
||||
VLOG = 19
|
||||
@@ -0,0 +1,83 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from sau_backend.conf import BASE_DIR
|
||||
|
||||
|
||||
def get_absolute_path(relative_path: str, base_dir: str = None) -> str:
|
||||
# Convert the relative path to an absolute path
|
||||
absolute_path = Path(BASE_DIR) / base_dir / relative_path
|
||||
return str(absolute_path)
|
||||
|
||||
|
||||
def get_title_and_hashtags(filename):
|
||||
"""
|
||||
获取视频标题和 hashtag
|
||||
|
||||
Args:
|
||||
filename: 视频文件名
|
||||
|
||||
Returns:
|
||||
视频标题和 hashtag 列表
|
||||
"""
|
||||
|
||||
# 获取视频标题和 hashtag txt 文件名
|
||||
txt_filename = filename.replace(".mp4", ".txt")
|
||||
|
||||
# 读取 txt 文件
|
||||
with open(txt_filename, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# 获取标题和 hashtag
|
||||
splite_str = content.strip().split("\n")
|
||||
title = splite_str[0]
|
||||
hashtags = splite_str[1].replace("#", "").split(" ")
|
||||
|
||||
return title, hashtags
|
||||
|
||||
|
||||
def generate_schedule_time_next_day(total_videos, videos_per_day, daily_times=None, timestamps=False, start_days=0):
|
||||
"""
|
||||
Generate a schedule for video uploads, starting from the next day.
|
||||
|
||||
Args:
|
||||
- total_videos: Total number of videos to be uploaded.
|
||||
- videos_per_day: Number of videos to be uploaded each day.
|
||||
- daily_times: Optional list of specific times of the day to publish the videos.
|
||||
- timestamps: Boolean to decide whether to return timestamps or datetime objects.
|
||||
- start_days: Start from after start_days.
|
||||
|
||||
Returns:
|
||||
- A list of scheduling times for the videos, either as timestamps or datetime objects.
|
||||
"""
|
||||
if videos_per_day <= 0:
|
||||
raise ValueError("videos_per_day should be a positive integer")
|
||||
|
||||
if daily_times is None:
|
||||
# Default times to publish videos if not provided
|
||||
daily_times = [6, 11, 14, 16, 22]
|
||||
|
||||
if videos_per_day > len(daily_times):
|
||||
raise ValueError("videos_per_day should not exceed the length of daily_times")
|
||||
|
||||
# Generate timestamps
|
||||
schedule = []
|
||||
current_time = datetime.now()
|
||||
|
||||
for video in range(total_videos):
|
||||
day = video // videos_per_day + start_days + 1 # +1 to start from the next day
|
||||
daily_video_index = video % videos_per_day
|
||||
|
||||
# Calculate the time for the current video
|
||||
hour = daily_times[daily_video_index]
|
||||
time_offset = timedelta(days=day, hours=hour - current_time.hour, minutes=-current_time.minute,
|
||||
seconds=-current_time.second, microseconds=-current_time.microsecond)
|
||||
timestamp = current_time + time_offset
|
||||
|
||||
schedule.append(timestamp)
|
||||
|
||||
if timestamps:
|
||||
schedule = [int(time.timestamp()) for time in schedule]
|
||||
return schedule
|
||||
@@ -0,0 +1,52 @@
|
||||
from pathlib import Path
|
||||
from sys import stdout
|
||||
from loguru import logger
|
||||
|
||||
from sau_backend.conf import BASE_DIR
|
||||
|
||||
|
||||
def log_formatter(record: dict) -> str:
|
||||
"""
|
||||
Formatter for log records.
|
||||
:param dict record: Log object containing log metadata & message.
|
||||
:returns: str
|
||||
"""
|
||||
colors = {
|
||||
"TRACE": "#cfe2f3",
|
||||
"INFO": "#9cbfdd",
|
||||
"DEBUG": "#8598ea",
|
||||
"WARNING": "#dcad5a",
|
||||
"SUCCESS": "#3dd08d",
|
||||
"ERROR": "#ae2c2c"
|
||||
}
|
||||
color = colors.get(record["level"].name, "#b3cfe7")
|
||||
return f"<fg #70acde>{{time:YYYY-MM-DD HH:mm:ss}}</fg #70acde> | <fg {color}>{{level}}</fg {color}>: <light-white>{{message}}</light-white>\n"
|
||||
|
||||
|
||||
def create_logger(log_name: str, file_path: str):
|
||||
"""
|
||||
Create custom logger for different business modules.
|
||||
:param str log_name: name of log
|
||||
:param str file_path: Optional path to log file
|
||||
:returns: Configured logger
|
||||
"""
|
||||
def filter_record(record):
|
||||
return record["extra"].get("business_name") == log_name
|
||||
|
||||
Path(BASE_DIR / file_path).parent.mkdir(exist_ok=True)
|
||||
logger.add(Path(BASE_DIR / file_path), filter=filter_record, level="INFO", rotation="10 MB", retention="10 days", backtrace=True, diagnose=True)
|
||||
return logger.bind(business_name=log_name)
|
||||
|
||||
|
||||
# Remove all existing handlers
|
||||
logger.remove()
|
||||
# Add a standard console handler
|
||||
logger.add(stdout, colorize=True, format=log_formatter)
|
||||
|
||||
douyin_logger = create_logger('douyin', 'logs/douyin.log')
|
||||
tencent_logger = create_logger('tencent', 'logs/tencent.log')
|
||||
xhs_logger = create_logger('xhs', 'logs/xhs.log')
|
||||
tiktok_logger = create_logger('tiktok', 'logs/tiktok.log')
|
||||
bilibili_logger = create_logger('bilibili', 'logs/bilibili.log')
|
||||
kuaishou_logger = create_logger('kuaishou', 'logs/kuaishou.log')
|
||||
baijiahao_logger = create_logger('baijiahao', 'logs/baijiahao.log')
|
||||
@@ -0,0 +1,28 @@
|
||||
import asyncio
|
||||
import time
|
||||
from functools import wraps
|
||||
|
||||
|
||||
def async_retry(timeout=60, max_retries=None):
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
start_time = time.time()
|
||||
attempts = 0
|
||||
while True:
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
attempts += 1
|
||||
if max_retries is not None and attempts >= max_retries:
|
||||
print(f"Reached maximum retries of {max_retries}.")
|
||||
raise Exception(f"Failed after {max_retries} retries.") from e
|
||||
if time.time() - start_time > timeout:
|
||||
print(f"Function timeout after {timeout} seconds.")
|
||||
raise TimeoutError(f"Function execution exceeded {timeout} seconds timeout.") from e
|
||||
print(f"Attempt {attempts} failed: {e}. Retrying...")
|
||||
await asyncio.sleep(1) # Sleep to avoid tight loop or provide backoff logic here
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
After Width: | Height: | Size: 103 KiB |
@@ -0,0 +1,2 @@
|
||||
这位勇敢的男子为了心爱之人每天坚守 🥺❤️🩹
|
||||
#坚持不懈 #爱情执着 #奋斗使者 #短视频
|
||||