From ec559e2913c70836b97ea2634604ac6ca6734a60 Mon Sep 17 00:00:00 2001 From: imgyh <1974355683@qq.com> Date: Tue, 28 Mar 2023 17:21:04 +0800 Subject: [PATCH] =?UTF-8?q?feat(tiktok):=20=E5=A2=9E=E5=8A=A0=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=96=87=E4=BB=B6,=20=E6=89=8B=E5=8A=A8=E4=BC=A0?= =?UTF-8?q?=E5=85=A5=E8=87=AA=E5=B7=B1=E7=9A=84cookie?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix #16 --- TikTokCommand.py | 272 +++++++++++++++++++++++++++++++++++++---------- config.yml | 81 ++++++++++++++ 2 files changed, 299 insertions(+), 54 deletions(-) create mode 100644 config.yml diff --git a/TikTokCommand.py b/TikTokCommand.py index 358886c..8119573 100644 --- a/TikTokCommand.py +++ b/TikTokCommand.py @@ -16,17 +16,40 @@ Change Log : import argparse import os import json +import yaml +import time from TikTok import TikTok from TikTokUtils import Utils +configModel = { + "link": [], + "path": os.getcwd(), + "music": True, + "cover": True, + "avatar": True, + "json": True, + "mode": ["post"], + "number": { + "post": 0, + "like": 0, + "allmix": 0, + "mix": 0, + "music": 0, + }, + "thread": 5, + "cookie": None + +} def argument(): parser = argparse.ArgumentParser(description='抖音批量下载工具 使用帮助') + parser.add_argument("--cmd", "-C", help="使用命令行(True)或者配置文件(False), 默认为False", + type=Utils().str2bool, required=False, default=False) parser.add_argument("--link", "-l", - help="作品(视频或图集)、直播、合集、音乐集合、个人主页的分享链接或者电脑浏览器网址(删除文案, 保证只有URL, https://v.douyin.com/kcvMpuN/ 或者 https://www.douyin.com/开头的)", - type=str, required=True) - parser.add_argument("--path", "-p", help="下载保存位置", - type=str, required=True) + help="作品(视频或图集)、直播、合集、音乐集合、个人主页的分享链接或者电脑浏览器网址, 可以设置多个链接(删除文案, 保证只有URL, https://v.douyin.com/kcvMpuN/ 或者 https://www.douyin.com/开头的)", + type=str, required=False, default=[], action="append") + parser.add_argument("--path", "-p", help="下载保存位置, 默认当前文件位置", + type=str, required=False,default=os.getcwd()) parser.add_argument("--music", "-m", help="是否下载视频中的音乐(True/False), 默认为True", type=Utils().str2bool, required=False, default=True) parser.add_argument("--cover", "-c", help="是否下载视频的封面(True/False), 默认为True, 当下载视频时有效", @@ -35,72 +58,213 @@ def argument(): type=Utils().str2bool, required=False, default=True) parser.add_argument("--json", "-j", help="是否保存获取到的数据(True/False), 默认为True", type=Utils().str2bool, required=False, default=True) - parser.add_argument("--mode", "-M", help="link是个人主页时, 设置下载发布的作品(post)或喜欢的作品(like)或者用户所有合集(mix), 默认为post", - type=str, required=False, default="post") - parser.add_argument("--number", "-n", - help="1.当下载单个合集、音乐集合、主页作品(post模式)和喜欢(like模式)时, 可设置下载前n个作品, 默认为0全部下载\r\n" + - "2.当下载主页下所有合集(mix模式)时, 设置下载前n个合集下所有作品, 默认为0全部下载", + parser.add_argument("--mode", "-M", help="link是个人主页时, 设置下载发布的作品(post)或喜欢的作品(like)或者用户所有合集(mix), 默认为post, 可以设置多种模式", + type=str, required=False, default=["post"], action="append") + parser.add_argument("--postnumber", help="主页下作品下载个数设置, 默认为0 全部下载", + type=int, required=False, default=0) + parser.add_argument("--likenumber", help="主页下喜欢下载个数设置, 默认为0 全部下载", + type=int, required=False, default=0) + parser.add_argument("--allmixnumber", help="主页下合集下载个数设置, 默认为0 全部下载", + type=int, required=False, default=0) + parser.add_argument("--mixnumber", help="单个合集下作品下载个数设置, 默认为0 全部下载", + type=int, required=False, default=0) + parser.add_argument("--musicnumber", help="音乐(原声)下作品下载个数设置, 默认为0 全部下载", type=int, required=False, default=0) parser.add_argument("--thread", "-t", help="设置线程数, 默认5个线程", type=int, required=False, default=5) + parser.add_argument("--cookie", help="设置cookie, 格式: \"name1=value1; name2=value2;\" 注意要加冒号", + type=str, required=False, default='') args = parser.parse_args() + if args.thread <= 0: + args.thread = 5 return args +def yamlConfig(): + curPath = os.path.dirname(os.path.abspath(__file__)) + yamlPath = os.path.join(curPath, "config.yml") + f = open(yamlPath, 'r', encoding='utf-8') + cfg = f.read() + configDict = yaml.load(stream=cfg,Loader=yaml.FullLoader) + + try: + configModel["link"] = configDict["link"] + except Exception as e: + print("[ 警告 ]:link未设置, 程序退出...\r\n") + try: + configModel["path"] = configDict["path"] + except Exception as e: + print("[ 警告 ]:path未设置, 使用当前路径...\r\n") + try: + configModel["music"] = configDict["music"] + except Exception as e: + print("[ 警告 ]:music未设置, 使用默认值True...\r\n") + try: + configModel["cover"] = configDict["cover"] + except Exception as e: + print("[ 警告 ]:cover未设置, 使用默认值True...\r\n") + try: + configModel["avatar"] = configDict["avatar"] + except Exception as e: + print("[ 警告 ]:avatar未设置, 使用默认值True...\r\n") + try: + configModel["json"] = configDict["json"] + except Exception as e: + print("[ 警告 ]:json未设置, 使用默认值True...\r\n") + try: + configModel["mode"] = configDict["mode"] + except Exception as e: + print("[ 警告 ]:mode未设置, 使用默认值post...\r\n") + try: + configModel["number"]["post"] = configDict["number"]["post"] + except Exception as e: + print("[ 警告 ]:post number未设置, 使用默认值0...\r\n") + try: + configModel["number"]["like"] = configDict["number"]["like"] + except Exception as e: + print("[ 警告 ]:like number未设置, 使用默认值0...\r\n") + try: + configModel["number"]["allmix"] = configDict["number"]["allmix"] + except Exception as e: + print("[ 警告 ]:allmix number未设置, 使用默认值0...\r\n") + try: + configModel["number"]["mix"] = configDict["number"]["mix"] + except Exception as e: + print("[ 警告 ]:mix number未设置, 使用默认值0...\r\n") + try: + configModel["number"]["music"] = configDict["number"]["music"] + except Exception as e: + print("[ 警告 ]:music number未设置, 使用默认值0...\r\n") + try: + configModel["thread"] = configDict["thread"] + except Exception as e: + print("[ 警告 ]:thread未设置, 使用默认值5...\r\n") + try: + cookiekey = configDict["cookies"].keys() + cookieStr = "" + for i in cookiekey: + cookieStr = cookieStr + i + "=" + configDict["cookies"][i] + "; " + configModel["cookie"] = cookieStr + except Exception as e: + pass + try: + configModel["cookie"] = configDict["cookie"] + except Exception as e: + pass + def main(): + start = time.time() # 开始时间 + utils = Utils() args = argument() - tk = TikTok() - url = tk.getShareLink(args.link) - key_type, key = tk.getKey(url) - if args.thread <= 0: - args.thread = 5 - if key is None or key_type is None: + + if args.cmd: + configModel["link"] = args.link + configModel["path"] = args.path + configModel["music"] = args.music + configModel["cover"] = args.cover + configModel["avatar"] = args.avatar + configModel["json"] = args.json + configModel["mode"] = args.mode + configModel["number"]["post"] = args.postnumber + configModel["number"]["like"] = args.likenumber + configModel["number"]["allmix"] = args.allmixnumber + configModel["number"]["mix"] = args.mixnumber + configModel["number"]["music"] = args.musicnumber + configModel["thread"] = args.thread + configModel["cookie"] = args.cookie + else: + yamlConfig() + + if configModel["link"] == []: return - elif key_type == "user" and args.mode != 'mix': - datalist = tk.getUserInfo(key, args.mode, 35, args.number) - tk.userDownload(awemeList=datalist, music=args.music, cover=args.cover, avatar=args.avatar, resjson=args.json, - savePath=args.path, thread=args.thread) - elif key_type == "user" and args.mode == 'mix': - if not os.path.exists(args.path): - os.mkdir(args.path) - mixIdNameDict = tk.getUserAllMixInfo(key, 35, args.number) - for mix_id in mixIdNameDict: - print(f'[ 提示 ]:正在下载合集 [{mixIdNameDict[mix_id]}] 中的作品\r\n') - mix_file_name = utils.replaceStr(mixIdNameDict[mix_id]) - datalist = tk.getMixInfo(mix_id, 35) - tk.userDownload(awemeList=datalist, music=args.music, cover=args.cover, avatar=args.avatar, resjson=args.json, - savePath=os.path.join(args.path, mix_file_name), thread=args.thread) - print(f'[ 提示 ]:合集 [{mixIdNameDict[mix_id]}] 中的作品下载完成\r\n') - elif key_type == "mix": - datalist = tk.getMixInfo(key,35, args.number) - tk.userDownload(awemeList=datalist, music=args.music, cover=args.cover, avatar=args.avatar, resjson=args.json, - savePath=args.path, thread=args.thread) - elif key_type == "music": - datalist = tk.getMusicInfo(key,35, args.number) - tk.userDownload(awemeList=datalist, music=args.music, cover=args.cover, avatar=args.avatar, resjson=args.json, - savePath=args.path, thread=args.thread) - elif key_type == "aweme": - datanew, dataraw = tk.getAwemeInfo(key) - datalist = [] - datalist.append(datanew) - tk.userDownload(awemeList=datalist, music=args.music, cover=args.cover, avatar=args.avatar, resjson=args.json, - savePath=args.path, thread=args.thread) - elif key_type == "live": - live_json = tk.getLiveInfo(key) - if args.json: - if not os.path.exists(args.path): - os.mkdir(args.path) + tk = TikTok() + tk.headers["Cookie"] = configModel["cookie"] - # 保存获取到json - print("[ 提示 ]:正在保存获取到的信息到result.json\r\n") - with open(os.path.join(args.path, "result.json"), "w", encoding='utf-8') as f: - f.write(json.dumps(live_json, ensure_ascii=False, indent=2)) - f.close() + if not os.path.exists(configModel["path"]): + os.mkdir(configModel["path"]) + for link in configModel["link"]: + print("[ 提示 ]:正在请求的链接: " + link + "\r\n") + url = tk.getShareLink(link) + key_type, key = tk.getKey(url) + if key_type == "user": + userPath = os.path.join(configModel["path"], "user_"+key) + if not os.path.exists(userPath): + os.mkdir(userPath) + + for mode in configModel["mode"]: + if mode == 'post' or mode == 'like': + datalist = tk.getUserInfo(key, mode, 35, configModel["number"][mode]) + if datalist is not None and datalist != []: + modePath = os.path.join(userPath, mode) + if not os.path.exists(modePath): + os.mkdir(modePath) + tk.userDownload(awemeList=datalist, music=configModel["music"], cover=configModel["cover"], + avatar=configModel["avatar"], resjson=configModel["json"], + savePath=modePath, thread=configModel["thread"]) + elif mode == 'mix': + mixIdNameDict = tk.getUserAllMixInfo(key, 35, configModel["number"]["allmix"]) + if mixIdNameDict is not None and mixIdNameDict != {}: + for mix_id in mixIdNameDict: + print(f'[ 提示 ]:正在下载合集 [{mixIdNameDict[mix_id]}] 中的作品\r\n') + mix_file_name = utils.replaceStr(mixIdNameDict[mix_id]) + datalist = tk.getMixInfo(mix_id, 35) + if datalist is not None and datalist != []: + modePath = os.path.join(userPath, mode) + if not os.path.exists(modePath): + os.mkdir(modePath) + tk.userDownload(awemeList=datalist, music=configModel["music"], cover=configModel["cover"], + avatar=configModel["avatar"], resjson=configModel["json"], + savePath=os.path.join(modePath, mix_file_name), thread=configModel["thread"]) + print(f'[ 提示 ]:合集 [{mixIdNameDict[mix_id]}] 中的作品下载完成\r\n') + elif key_type == "mix": + datalist = tk.getMixInfo(key,35, configModel["number"]["mix"]) + if datalist is not None and datalist != []: + mixPath = os.path.join(configModel["path"], "mix_" + key) + if not os.path.exists(mixPath): + os.mkdir(mixPath) + tk.userDownload(awemeList=datalist, music=configModel["music"], cover=configModel["cover"], + avatar=configModel["avatar"], resjson=configModel["json"], + savePath=mixPath, thread=configModel["thread"]) + elif key_type == "music": + datalist = tk.getMusicInfo(key,35, configModel["number"]["music"]) + if datalist is not None and datalist != []: + musicPath = os.path.join(configModel["path"], "music_" + key) + if not os.path.exists(musicPath): + os.mkdir(musicPath) + tk.userDownload(awemeList=datalist, music=configModel["music"], cover=configModel["cover"], + avatar=configModel["avatar"], resjson=configModel["json"], + savePath=musicPath, thread=configModel["thread"]) + elif key_type == "aweme": + datanew, dataraw = tk.getAwemeInfo(key) + if datanew is not None and datanew != {}: + datalist = [] + datalist.append(datanew) + awemePath = os.path.join(configModel["path"], "aweme") + if not os.path.exists(awemePath): + os.mkdir(awemePath) + tk.userDownload(awemeList=datalist, music=configModel["music"], cover=configModel["cover"], + avatar=configModel["avatar"], resjson=configModel["json"], + savePath=awemePath, thread=configModel["thread"]) + elif key_type == "live": + live_json = tk.getLiveInfo(key) + if configModel["json"]: + livePath = os.path.join(configModel["path"], "live") + if not os.path.exists(livePath): + os.mkdir(livePath) + live_file_name = utils.replaceStr(key + live_json["nickname"]) + # 保存获取到json + print("[ 提示 ]:正在保存获取到的信息到result.json\r\n") + with open(os.path.join(livePath, live_file_name + ".json"), "w", encoding='utf-8') as f: + f.write(json.dumps(live_json, ensure_ascii=False, indent=2)) + f.close() + + end = time.time() # 结束时间 + print('\n' + '[下载完成]:总耗时: %d分钟%d秒\n' % (int((end - start) / 60), ((end - start) % 60))) # 输出下载用时时间 if __name__ == "__main__": main() diff --git a/config.yml b/config.yml new file mode 100644 index 0000000..ba5f389 --- /dev/null +++ b/config.yml @@ -0,0 +1,81 @@ +####################################### +# 说明: +# 1. 井号(#)为注释 +# 2. 缩进严格对齐,使用空格缩进, 注意有些冒号后面有一个空格, 有些没有空格 +# 3. 请使用英文字符 +# 4. 更多yaml语法请上网查看 +####################################### + + +# 作品(视频或图集)、直播、合集、音乐集合、个人主页的分享链接或者电脑浏览器网址 +# (删除文案, 保证只有URL, https://v.douyin.com/kcvMpuN/ 或者 https://www.douyin.com/开头的) +# 可以设置多个链接, 确保至少一个链接 +# 必选 +link: + - https://live.douyin.com/759547612580 + - https://v.douyin.com/BugmVVD/ + - https://v.douyin.com/BugrFTN/ + - https://v.douyin.com/B72pdU5/ + - https://v.douyin.com/B72QgDw/ + - https://v.douyin.com/AJp8D3f/ + - https://v.douyin.com/B38oovu/ + - https://v.douyin.com/S6YMNXs/ + +# 下载保存位置, 默认当前文件位置 +# 必选 +path: /mnt/c/project/test333 + +# 是否下载视频中的音乐(True/False), 默认为True +# 可选 +music: True + +# 是否下载视频的封面(True/False), 默认为True, 当下载视频时有效 +# 可选 +cover: True + +# 是否下载作者的头像(True/False), 默认为True +# 可选 +avatar: True + +# 是否保存获取到的数据(True/False), 默认为True +# 可选 +json: True + +# link是个人主页时, 设置下载发布的作品(post)或喜欢的作品(like)或者用户所有合集(mix), 默认为post, 可以设置多种模式 +# 可选 +mode: + - post + - like + - mix + +# 下载作品个数设置 +# 可选 +number: + post: 5 # 主页下作品下载个数设置, 默认为0 全部下载 + like: 5 # 主页下喜欢下载个数设置, 默认为0 全部下载 + allmix: 1 # 主页下合集下载个数设置, 默认为0 全部下载 + mix: 5 # 单个合集下作品下载个数设置, 默认为0 全部下载 + music: 5 # 音乐(原声)下作品下载个数设置, 默认为0 全部下载 + +# 设置线程数, 默认5个线程 +# 可选 +thread: 5 + +# cookie 请登录网页抖音后F12查看 +# cookies 和 cookie 二选一, 要使用这种形式, 请注释下面的cookie +# 目前只需要msToken、ttwid、odin_tt、passport_csrf_token、sid_guard +# 可以动态添加, 程序会根据填的键查找,并没有写死, 如果抖音需要更多的cookie自己加上就行了 +cookies: + msToken: xxx + ttwid: xxx + odin_tt: xxx + passport_csrf_token: xxx + sid_guard: xxx + +# cookie 请登录网页抖音后F12查看 +# cookies 和 cookie 二选一, 要使用这种形式, 请注释上面的cookies及包含的所有键值对 +# 设置了这个后上面的cookies选项自动失效, 这个优先级更高 +# 格式: "name1=value1; name2=value2;" 注意要加冒号 +# 冒号中的内容包括不限于以下键值对, 如果抖音需要更多的cookie自己加上就行了 +#cookie: "msToken=xxx; ttwid=xxx; odin_tt=xxx; passport_csrf_token=xxx; sid_guard=xxx;" +