def exit_ftp(show_err=True): try: ftp.quit() except BaseException as e: if show_err and self._settings['ftp']['show_error_detail']: err_print(self._sn, 'FTP狀態', '將强制關閉FTP連接, 因爲在退出時收到異常: ' + str(e)) ftp.close()
def check_new_version(): # 检查GitHub上是否有新版 remote_version = Config.read_latest_version_on_github() if float(settings['aniGamerPlus_version'][1:]) < float( remote_version['tag_name'][1:]): msg = '發現GitHub上有新版本: ' + remote_version[ 'tag_name'] + '\n更新内容:\n' + remote_version['body'] + '\n' err_print(0, msg, status=1, no_sn=True)
def recv_config(): data = json.loads(request.get_data(as_text=True)) new_settings = Config.read_settings() for id in id_list: new_settings[id] = data[id] # 更新配置 Config.write_settings(new_settings) # 保存配置 err_print(0, 'Dashboard', '通過 Web 控制臺更新了 config.json', no_sn=True, status=2) return '{"status":"200"}'
def __get_title(self): soup = self._src try: self._title = soup.find('div', 'anime_name').h1.string # 提取标题(含有集数) except TypeError: # 该sn下没有动画 err_print(self._sn, 'ERROR: 該 sn 下真的有動畫?', status=1) self._episode_list = {} sys.exit(1)
def __get_title(self): soup = self._src try: self._title = soup.find( 'meta', property="og:title")['content'] # 提取标题(含有集数) except TypeError: # 该sn下没有动画 err_print(self._sn, 'ERROR: 該 sn 下真的有動畫?', status=1) self._episode_list = {} sys.exit(1)
def build_anime(sn): anime = {'anime': None, 'failed': True} try: if settings['use_gost']: # 如果使用 gost, 则随机一个 gost 监听端口 anime['anime'] = Anime(sn, gost_port=gost_port) else: anime['anime'] = Anime(sn) anime['failed'] = False except TryTooManyTimeError: err_print(sn, '抓取失敗', '影片信息抓取失敗!', status=1) except BaseException as e: err_print(sn, '抓取失敗', '抓取影片信息時發生未知錯誤: '+str(e), status=1) return anime
def check_no_ad(): req = "https://ani.gamer.com.tw/ajax/token.php?sn=" + str( self._sn) + "&device=" + self._device_id + "&hash=" + random_string(12) f = self.__request(req) resp = f.json() if 'time' in resp.keys(): if resp['time'] == 1: # print('check_no_ad: Adaway!') pass else: err_print(self._sn, 'check_no_ad: Ads not away?', status=1) else: # print('check_no_ad: Not in right area.') err_print(self._sn, '遭到動畫瘋地區限制, 你的IP可能不被動畫瘋認可!', status=1) sys.exit(1)
def check_tasks(): for sn in sn_dict.keys(): anime = build_anime(sn) if anime['failed']: err_print(sn, '更新狀態', '檢查更新失敗, 跳過等待下次檢查', status=1) continue anime = anime['anime'] err_print(sn, '更新資訊', '正在檢查《' + anime.get_bangumi_name() + '》') episode_list = list(anime.get_episode_list().values()) if sn_dict[sn]['mode'] == 'all': # 如果用户选择全部下载 download_mode = 'all' for ep in episode_list: # 遍历剧集列表 try: db = read_db(ep) # 未下载的 或 设定要上传但是没上传的 并且 还没在列队中 if (db['status'] == 0 or (db['remote_status'] == 0 and settings['upload_to_server'])) and ep not in queue.keys(): queue[ep] = sn_dict[sn] # 添加至下载列队 except IndexError: # 如果数据库中尚不存在此条记录 if anime.get_sn() == ep: new_anime = anime # 如果是本身则不用重复创建实例 else: new_anime = build_anime(ep) if new_anime['failed']: err_print(ep, '更新狀態', '更新數據失敗, 跳過等待下次檢查', status=1) continue new_anime = new_anime['anime'] insert_db(new_anime) queue[ep] = sn_dict[sn] # 添加至列队 else: if sn_dict[sn]['mode'] == 'largest-sn': # 如果用户选择仅下载最新上传, download_mode = 'largest_sn', 则对 sn 进行排序 episode_list.sort() latest_sn = episode_list[-1] # 否则用户选择仅下载最后剧集, download_mode = 'latest', 即下载网页上显示在最右的剧集 elif sn_dict[sn]['mode'] == 'single': latest_sn = sn # 适配命令行 sn-list 模式 else: latest_sn = episode_list[-1] try: db = read_db(latest_sn) # 未下载的 或 设定要上传但是没上传的 并且 还没在列队中 if (db['status'] == 0 or (db['remote_status'] == 0 and settings['upload_to_server'])) and latest_sn not in queue.keys(): queue[latest_sn] = sn_dict[sn] # 添加至下载列队 except IndexError: # 如果数据库中尚不存在此条记录 if anime.get_sn() == latest_sn: new_anime = anime # 如果是本身则不用重复创建实例 else: new_anime = build_anime(latest_sn) if new_anime['failed']: err_print(latest_sn, '更新狀態', '更新數據失敗, 跳過等待下次檢查', status=1) continue new_anime = new_anime['anime'] insert_db(new_anime) queue[latest_sn] = sn_dict[sn]
def __color_print(sn, err_msg, detail='', status=0, no_sn=False, display=True): # 避免与 ColorPrint.py 相互调用产生问题 try: err_print(sn, err_msg, detail=detail, status=status, no_sn=no_sn, display=display) except UnboundLocalError: from ColorPrint import err_print err_print(sn, err_msg, detail=detail, status=status, no_sn=no_sn, display=display)
def __init_proxy(): if settings['use_gost']: print('使用代理連接動畫瘋, 使用擴展的代理協議') # 需要使用 gost 的情况 # 寻找 gost check_gost = subprocess.Popen('gost -h', shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) if check_gost.stderr.readlines(): # 查找 ffmpeg 是否已放入系统 path gost_path = 'gost' else: # print('没有在系统PATH中发现gost,尝试在所在目录寻找') if 'Windows' in platform.system(): gost_path = os.path.join(working_dir, 'gost.exe') else: gost_path = os.path.join(working_dir, 'gost') if not os.path.exists(gost_path): err_print(0, '當前代理使用擴展協議, 需要使用gost, 但是gost未找到', status=1, no_sn=True) raise FileNotFoundError # 如果本地目录下也没有找到 gost 则丢出异常 # 构造 gost 命令 gost_cmd = [gost_path, '-L=:' + str(gost_port)] # 本地监听端口 34173 proxies_keys = list(settings['proxies'].keys()) proxies_keys.sort() # 排序, 确保链式结构正确 for key in proxies_keys: gost_cmd.append('-F=' + settings['proxies'][key]) # 构建(链式)代理 def run_gost(): # gost 线程 global gost_subprocess gost_subprocess = subprocess.Popen(gost_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) gost_subprocess.communicate() run_gost_threader = threading.Thread(target=run_gost) run_gost_threader.setDaemon(True) run_gost_threader.start() # 启动 gost time.sleep(3) # 给时间让 gost 启动 else: print('使用代理連接動畫瘋, 使用http/https/socks5協議')
def run_dashboard(): from Dashboard.Server import run as dashboard server = threading.Thread(target=dashboard) server.setDaemon(True) server.start() if settings['dashboard']['SSL']: dashboard_address = 'https://' else: dashboard_address = 'http://' if settings['dashboard']['host'] == '0.0.0.0': host = Config.get_local_ip() dashboard_address = '【開放外部訪問】訪問地址: ' + dashboard_address else: host = settings['dashboard']['host'] dashboard_address = '訪問地址: ' + dashboard_address dashboard_address = dashboard_address + host + ':' + str( settings['dashboard']['port']) err_print(0, 'Web控制面板已啓動', dashboard_address, no_sn=True, status=2)
def download_chunk(uri): limiter.acquire() chunk_name = re.findall(r'media_b.+ts', uri)[0] # chunk 文件名 chunk_local_path = os.path.join(temp_dir, chunk_name) # chunk 路径 nonlocal failed_flag try: with open(chunk_local_path, 'wb') as f: f.write( self.__request(uri, no_cookies=True, show_fail=False, max_retry=8).content) except TryTooManyTimeError: failed_flag = True err_print(self._sn, '下載狀態', 'Bad segment=' + chunk_name, status=1) limiter.release() sys.exit(1) except BaseException as e: failed_flag = True err_print(self._sn, '下載狀態', 'Bad segment=' + chunk_name + ' 發生未知錯誤: ' + str(e), status=1) limiter.release() sys.exit(1) if self.realtime_show_file_size: # 显示完成百分比 nonlocal finished_chunk_counter finished_chunk_counter = finished_chunk_counter + 1 progress_rate = float(finished_chunk_counter / total_chunk_num * 100) progress_rate = round(progress_rate, 2) sys.stdout.write('\r正在下載: sn=' + str(self._sn) + ' ' + filename + ' ' + str(progress_rate) + '% ') sys.stdout.flush() limiter.release()
def insert_db(anime): db_locker.acquire() # 向数据库插入新资料 anime_dict = {'sn': str(anime.get_sn()), 'title': anime.get_title(), 'anime_name': anime.get_bangumi_name(), 'episode': anime.get_episode()} conn = sqlite3.connect(db_path) cursor = conn.cursor() try: cursor.execute("INSERT INTO anime (sn, title, anime_name, episode) VALUES (:sn, :title, :anime_name, :episode)", anime_dict) except sqlite3.IntegrityError as e: err_print(anime_dict['sn'], 'DB错误', 'title=' + anime_dict['title'] + ' 数据已存在!' + str(e), status=1) cursor.close() conn.commit() conn.close() db_locker.release()
def check_ffmpeg_alive(): # 应对ffmpeg卡死, 资源限速等,若 1min 中内文件大小没有增加超过 3M, 则判定卡死 if self.realtime_show_file_size: # 是否实时显示文件大小, 设计仅 cui 下载单个文件或线程数=1时适用 sys.stdout.write('正在下載: sn=' + str(self._sn) + ' ' + filename) sys.stdout.flush() else: err_print(self._sn, '正在下載', filename + ' title=' + self._title) time.sleep(2) time_counter = 1 pre_temp_file_size = 0 while run_ffmpeg.poll() is None: if self.realtime_show_file_size: # 实时显示文件大小 if os.path.exists(downloading_file): size = os.path.getsize(downloading_file) size = size / float(1024 * 1024) size = round(size, 2) sys.stdout.write('\r正在下載: sn=' + str(self._sn) + ' ' + filename + ' ' + str(size) + 'MB ') sys.stdout.flush() else: sys.stdout.write('\r正在下載: sn=' + str(self._sn) + ' ' + filename + ' 文件尚未生成 ') sys.stdout.flush() if time_counter % 60 == 0 and os.path.exists(downloading_file): temp_file_size = os.path.getsize(downloading_file) a = temp_file_size - pre_temp_file_size if a < (3 * 1024 * 1024): err_msg_detail = downloading_filename + ' 在一分钟内仅增加' + str( int(a / float(1024))) + 'KB 判定为卡死, 任务失败!' err_print(self._sn, '下載失败', err_msg_detail, status=1) run_ffmpeg.kill() return pre_temp_file_size = temp_file_size time.sleep(1) time_counter = time_counter + 1
def get_info(self): err_print(self._sn, '顯示資訊') err_print(0, ' 影片標題:', self.get_title(), no_sn=True, display_time=False) err_print(0, ' 番劇名稱:', self.get_bangumi_name(), no_sn=True, display_time=False) err_print(0, ' 劇集標題:', self.get_episode(), no_sn=True, display_time=False) err_print(0, ' 可用解析度', 'P '.join(self.get_m3u8_dict().keys()) + 'P\n', no_sn=True, display_time=False)
def manual_task(): data = json.loads(request.get_data(as_text=True)) settings = Config.read_settings() # 下载清晰度 if data['resolution'] not in ('360', '480', '540', '720', '1080'): # 如果不是合法清晰度 resolution = settings['download_resolution'] else: resolution = data['resolution'] # 下载模式 if data['mode'] not in ('single', 'latest', 'all', 'largest-sn'): mode = 'single' else: mode = data['mode'] # 下载线程数 thread = int(data['thread']) if thread > Config.get_max_multi_thread(): # 是否超过最大允许线程数 thread_limit = Config.get_max_multi_thread() else: thread_limit = thread def run_cui(): cui(data['sn'], resolution, mode, thread_limit, [], classify=data['classify'], realtime_show=False, cui_danmu=data['danmu']) server = threading.Thread(target=run_cui) err_print(0, 'Dashboard', '通過 Web 控制臺下達了手動任務', no_sn=True, status=2) server.start() # 启动手动任务线程 return '{"status":"200"}'
def run_dashboard(): # 检测端口是否占用 if not port_is_available(settings['dashboard']['port']): err_print(0, 'Web控制面板啓動失敗', 'Port已被占用! 請到配置文件更換', status=1, no_sn=True) return from Dashboard.Server import run as dashboard server = threading.Thread(target=dashboard) server.setDaemon(True) server.start() if settings['dashboard']['SSL']: dashboard_address = 'https://' else: dashboard_address = 'http://' if settings['dashboard']['host'] == '0.0.0.0': host = Config.get_local_ip() dashboard_address = '【開放外部訪問】訪問地址: ' + dashboard_address else: host = settings['dashboard']['host'] dashboard_address = '訪問地址: ' + dashboard_address dashboard_address = dashboard_address + host + ':' + str(settings['dashboard']['port']) err_print(0, 'Web控制面板已啓動', dashboard_address, no_sn=True, status=2)
def __download_only(sn, dl_resolution='', dl_save_dir='', realtime_show_file_size=False, classify=True): # 仅下载,不操作数据库 thread_limiter.acquire() err_counter = 0 anime = build_anime(sn) if anime['failed']: sys.exit(1) anime = anime['anime'] try: if dl_resolution: anime.download(dl_resolution, dl_save_dir, realtime_show_file_size=realtime_show_file_size, classify=classify) else: anime.download(settings['download_resolution'], dl_save_dir, realtime_show_file_size=realtime_show_file_size, classify=classify) except BaseException as e: err_print(sn, '下載異常', '發生未知異常: ' + str(e), status=1) anime.video_size = 0 while anime.video_size < 10: if err_counter >= 3: err_print(sn, '終止任務', 'title=' + anime.get_title()+' 任務失敗達三次! 終止任務!', status=1) thread_limiter.release() return else: err_print(sn, '任務失敗', 'title=' + anime.get_title() + ' 10s后自動重啓,最多重試三次', status=1) err_counter = err_counter + 1 time.sleep(10) anime.renew() try: if dl_resolution: anime.download(dl_resolution, dl_save_dir, realtime_show_file_size=realtime_show_file_size, classify=classify) else: anime.download(settings['download_resolution'], dl_save_dir, realtime_show_file_size=realtime_show_file_size, classify=classify) except BaseException as e: err_print(sn, '下載異常', '發生未知異常: ' + str(e), status=1) anime.video_size = 0 thread_limiter.release()
def build_anime(sn): anime = {'anime': None, 'failed': True} try: if settings['use_gost']: # 如果使用 gost, 则随机一个 gost 监听端口 anime['anime'] = Anime(sn, gost_port=gost_port) else: anime['anime'] = Anime(sn) anime['failed'] = False if danmu: anime['anime'].enable_danmu() except TryTooManyTimeError: err_print(sn, '抓取失敗', '影片信息抓取失敗!', status=1) except BaseException as e: err_print(sn, '抓取失敗', '抓取影片信息時發生未知錯誤: ' + str(e), status=1) err_print(sn, '抓取異常', '異常詳情:\n' + traceback.format_exc(), status=1) return anime
def __download_only(sn, dl_resolution='', dl_save_dir='', realtime_show_file_size=False, classify=True): # 仅下载,不操作数据库 thread_limiter.acquire() err_counter = 0 anime = build_anime(sn) if anime['failed']: sys.exit(1) anime = anime['anime'] try: if dl_resolution: anime.download(dl_resolution, dl_save_dir, realtime_show_file_size=realtime_show_file_size, classify=classify) else: anime.download(settings['download_resolution'], dl_save_dir, realtime_show_file_size=realtime_show_file_size, classify=classify) except BaseException as e: err_print(sn, '下載異常', '發生未知異常: ' + str(e), status=1) err_print(sn, '下載異常', '異常詳情:\n' + traceback.format_exc(), status=1, display=False) anime.video_size = 0 while anime.video_size < 5: if err_counter >= 3: err_print(sn, '終止任務', 'title=' + anime.get_title() + ' 任務失敗達三次! 終止任務!', status=1) thread_limiter.release() if int(sn) in Config.tasks_progress_rate.keys(): del Config.tasks_progress_rate[int(sn)] return else: err_print(sn, '任務失敗', 'title=' + anime.get_title() + ' 10s后自動重啓,最多重試三次', status=1) err_counter = err_counter + 1 if int(sn) in Config.tasks_progress_rate.keys(): Config.tasks_progress_rate[int(sn)]['status'] = '失敗! 重啓中' time.sleep(10) anime.renew() try: if dl_resolution: anime.download( dl_resolution, dl_save_dir, realtime_show_file_size=realtime_show_file_size, classify=classify) else: anime.download( settings['download_resolution'], dl_save_dir, realtime_show_file_size=realtime_show_file_size, classify=classify) except BaseException as e: err_print(sn, '下載異常', '發生未知異常: ' + str(e), status=1) err_print(sn, '下載異常', '異常詳情:\n' + traceback.format_exc(), status=1, display=False) anime.video_size = 0 thread_limiter.release()
def connect_ftp(show_err=True): ftp.encoding = 'utf-8' # 解决中文乱码 err_counter = 0 connect_flag = False while err_counter <= 3: try: ftp.connect(self._settings['ftp']['server'], self._settings['ftp']['port']) # 连接 FTP ftp.login(self._settings['ftp']['user'], self._settings['ftp']['pwd']) # 登陆 connect_flag = True break except ftplib.error_temp as e: if show_err: if 'Too many connections' in str(e): detail = self._video_filename + ' 当前FTP連接數過多, 5分鐘后重試, 最多重試三次: ' + str( e) err_print(self._sn, 'FTP狀態', detail, status=1) else: detail = self._video_filename + ' 連接FTP時發生錯誤, 5分鐘后重試, 最多重試三次: ' + str( e) err_print(self._sn, 'FTP狀態', detail, status=1) err_counter = err_counter + 1 for i in range(5 * 60): time.sleep(1) except BaseException as e: if show_err: detail = self._video_filename + ' 在連接FTP時發生無法處理的異常:' + str( e) err_print(self._sn, 'FTP狀態', detail, status=1) break if not connect_flag: err_print(self._sn, '上傳失败', self._video_filename, status=1) return connect_flag # 如果连接失败, 直接放弃 ftp.voidcmd('TYPE I') # 二进制模式 if self._settings['ftp']['cwd']: try: ftp.cwd(self._settings['ftp']['cwd']) # 进入用户指定目录 except ftplib.error_perm as e: if show_err: err_print(self._sn, 'FTP狀態', '進入指定FTP目錄時出錯: ' + str(e), status=1) if bangumi_tag: # 番剧分类 try: ftp.cwd(bangumi_tag) except ftplib.error_perm: try: ftp.mkd(bangumi_tag) ftp.cwd(bangumi_tag) except ftplib.error_perm as e: if show_err: err_print(self._sn, 'FTP狀態', '創建目錄番劇目錄時發生異常, 你可能沒有權限創建目錄: ' + str(e), status=1) # 归类番剧 ftp_bangumi_dir = Config.legalize_filename( self._bangumi_name) # 保证合法 try: ftp.cwd(ftp_bangumi_dir) except ftplib.error_perm: try: ftp.mkd(ftp_bangumi_dir) ftp.cwd(ftp_bangumi_dir) except ftplib.error_perm as e: if show_err: detail = '你可能沒有權限創建目錄(用於分類番劇), 視頻文件將會直接上傳, 收到異常: ' + str( e) err_print(self._sn, 'FTP狀態', detail, status=1) # 删除旧的临时文件夹 nonlocal first_connect if first_connect: # 首次连接 remove_dir(tmp_dir) first_connect = False # 标记第一次连接已完成 # 创建新的临时文件夹 # 创建临时文件夹是因为 pure-ftpd 在续传时会将文件名更改成不可预测的名字 # 正常中斷传输会把名字改回来, 但是意外掉线不会, 为了处理这种情况 # 需要获取 pure-ftpd 未知文件名的续传缓存文件, 为了不和其他视频的缓存文件混淆, 故建立一个临时文件夹 try: ftp.cwd(tmp_dir) except ftplib.error_perm: ftp.mkd(tmp_dir) ftp.cwd(tmp_dir) return connect_flag
def upload(self, bangumi_tag='', debug_file=''): first_connect = True # 标记是否是第一次连接, 第一次连接会删除临时缓存目录 tmp_dir = str(self._sn) + '-uploading-by-aniGamerPlus' if debug_file: self.local_video_path = debug_file if not os.path.exists(self.local_video_path): # 如果文件不存在,直接返回失败 return self.upload_succeed_flag if not self._video_filename: # 用于仅上传, 将文件名提取出来 self._video_filename = os.path.split(self.local_video_path)[-1] socket.setdefaulttimeout(20) # 超时时间20s if self._settings['ftp']['tls']: ftp = FTP_TLS() # FTP over TLS else: ftp = FTP() def connect_ftp(show_err=True): ftp.encoding = 'utf-8' # 解决中文乱码 err_counter = 0 connect_flag = False while err_counter <= 3: try: ftp.connect(self._settings['ftp']['server'], self._settings['ftp']['port']) # 连接 FTP ftp.login(self._settings['ftp']['user'], self._settings['ftp']['pwd']) # 登陆 connect_flag = True break except ftplib.error_temp as e: if show_err: if 'Too many connections' in str(e): detail = self._video_filename + ' 当前FTP連接數過多, 5分鐘后重試, 最多重試三次: ' + str( e) err_print(self._sn, 'FTP狀態', detail, status=1) else: detail = self._video_filename + ' 連接FTP時發生錯誤, 5分鐘后重試, 最多重試三次: ' + str( e) err_print(self._sn, 'FTP狀態', detail, status=1) err_counter = err_counter + 1 for i in range(5 * 60): time.sleep(1) except BaseException as e: if show_err: detail = self._video_filename + ' 在連接FTP時發生無法處理的異常:' + str( e) err_print(self._sn, 'FTP狀態', detail, status=1) break if not connect_flag: err_print(self._sn, '上傳失败', self._video_filename, status=1) return connect_flag # 如果连接失败, 直接放弃 ftp.voidcmd('TYPE I') # 二进制模式 if self._settings['ftp']['cwd']: try: ftp.cwd(self._settings['ftp']['cwd']) # 进入用户指定目录 except ftplib.error_perm as e: if show_err: err_print(self._sn, 'FTP狀態', '進入指定FTP目錄時出錯: ' + str(e), status=1) if bangumi_tag: # 番剧分类 try: ftp.cwd(bangumi_tag) except ftplib.error_perm: try: ftp.mkd(bangumi_tag) ftp.cwd(bangumi_tag) except ftplib.error_perm as e: if show_err: err_print(self._sn, 'FTP狀態', '創建目錄番劇目錄時發生異常, 你可能沒有權限創建目錄: ' + str(e), status=1) # 归类番剧 ftp_bangumi_dir = Config.legalize_filename( self._bangumi_name) # 保证合法 try: ftp.cwd(ftp_bangumi_dir) except ftplib.error_perm: try: ftp.mkd(ftp_bangumi_dir) ftp.cwd(ftp_bangumi_dir) except ftplib.error_perm as e: if show_err: detail = '你可能沒有權限創建目錄(用於分類番劇), 視頻文件將會直接上傳, 收到異常: ' + str( e) err_print(self._sn, 'FTP狀態', detail, status=1) # 删除旧的临时文件夹 nonlocal first_connect if first_connect: # 首次连接 remove_dir(tmp_dir) first_connect = False # 标记第一次连接已完成 # 创建新的临时文件夹 # 创建临时文件夹是因为 pure-ftpd 在续传时会将文件名更改成不可预测的名字 # 正常中斷传输会把名字改回来, 但是意外掉线不会, 为了处理这种情况 # 需要获取 pure-ftpd 未知文件名的续传缓存文件, 为了不和其他视频的缓存文件混淆, 故建立一个临时文件夹 try: ftp.cwd(tmp_dir) except ftplib.error_perm: ftp.mkd(tmp_dir) ftp.cwd(tmp_dir) return connect_flag def exit_ftp(show_err=True): try: ftp.quit() except BaseException as e: if show_err and self._settings['ftp']['show_error_detail']: err_print(self._sn, 'FTP狀態', '將强制關閉FTP連接, 因爲在退出時收到異常: ' + str(e)) ftp.close() def remove_dir(dir_name): try: ftp.rmd(dir_name) except ftplib.error_perm as e: if 'Directory not empty' in str(e): # 如果目录非空, 则删除内部文件 ftp.cwd(dir_name) del_all_files() ftp.cwd('..') ftp.rmd(dir_name) # 删完内部文件, 删除文件夹 elif 'No such file or directory' in str(e): pass else: # 其他非空目录报错 raise e def del_all_files(): try: for file_need_del in ftp.nlst(): if not re.match(r'^(\.|\.\.)$', file_need_del): ftp.delete(file_need_del) # print('删除了文件: ' + file_need_del) except ftplib.error_perm as resp: if not str(resp) == "550 No files found": raise if not connect_ftp(): # 连接 FTP return self.upload_succeed_flag # 如果连接失败 err_print(self._sn, '正在上傳', self._video_filename + ' title=' + self._title + '……') try_counter = 0 video_filename = self._video_filename # video_filename 将可能会储存 pure-ftpd 缓存文件名 max_try_num = self._settings['ftp']['max_retry_num'] local_size = os.path.getsize(self.local_video_path) # 本地文件大小 while try_counter <= max_try_num: try: if try_counter > 0: # 传输遭中断后处理 detail = self._video_filename + ' 发生异常, 重連FTP, 續傳文件, 將重試最多' + str( max_try_num) + '次……' err_print(self._sn, '上傳狀態', detail, status=1) if not connect_ftp(): # 重连 return self.upload_succeed_flag # 解决操蛋的 Pure-Ftpd 续传一次就改名导致不能再续传问题. # 一般正常关闭文件传输 Pure-Ftpd 会把名字改回来, 但是遇到网络意外中断, 那么就不会改回文件名, 留着临时文件名 # 本段就是处理这种情况 try: for i in ftp.nlst(): if 'pureftpd-upload' in i: # 找到 pure-ftpd 缓存, 直接抓缓存来续传 video_filename = i except ftplib.error_perm as resp: if not str(resp ) == "550 No files found": # 非文件不存在错误, 抛出异常 raise # 断点续传 try: # 需要 FTP Server 支持续传 ftp_binary_size = ftp.size(video_filename) # 远程文件字节数 except ftplib.error_perm: # 如果不存在文件 ftp_binary_size = 0 except OSError: try_counter = try_counter + 1 continue ftp.voidcmd('TYPE I') # 二进制模式 conn = ftp.transfercmd('STOR ' + video_filename, ftp_binary_size) # ftp服务器文件名和offset偏移地址 with open(self.local_video_path, 'rb') as f: f.seek(ftp_binary_size) # 从断点处开始读取 while True: block = f.read(1048576) # 读取1M conn.sendall(block) # 送出 block if not block: time.sleep(3) # 等待一下, 让sendall()完成 break conn.close() err_print(self._sn, '上傳狀態', '檢查遠端文件大小是否與本地一致……') exit_ftp(False) connect_ftp(False) # 不重连的话, 下面查询远程文件大小会返回 None, 懵逼... # sendall()没有完成将会 500 Unknown command err_counter = 0 remote_size = 0 while err_counter < 3: try: remote_size = ftp.size(video_filename) # 远程文件大小 break except ftplib.error_perm as e1: err_print(self._sn, 'FTP狀態', 'ftplib.error_perm: ' + str(e1)) remote_size = 0 break except OSError as e2: err_print(self._sn, 'FTP狀態', 'OSError: ' + str(e2)) remote_size = 0 connect_ftp(False) # 掉线重连 err_counter = err_counter + 1 if remote_size is None: err_print(self._sn, 'FTP狀態', 'remote_size is None') remote_size = 0 # 远程文件大小获取失败, 可能文件不存在或者抽风 # 那上面获取远程字节数将会是0, 导致重新下载, 那么此时应该清空缓存目录下的文件 # 避免后续找错文件续传 if remote_size == 0: del_all_files() if remote_size != local_size: # 如果远程文件大小与本地不一致 # print('remote_size='+str(remote_size)) # print('local_size ='+str(local_size)) detail = self._video_filename + ' 在遠端為' + str( round(remote_size / float(1024 * 1024), 2)) + 'MB' + ' 與本地' + str( round(local_size / float(1024 * 1024), 2) ) + 'MB 不一致! 將重試最多' + str(max_try_num) + '次' err_print(self._sn, '上傳狀態', detail, status=1) try_counter = try_counter + 1 continue # 续传 # 顺利上传完后 ftp.cwd('..') # 返回上级目录, 即退出临时目录 try: # 如果同名文件存在, 则删除 ftp.size(self._video_filename) ftp.delete(self._video_filename) except ftplib.error_perm: pass ftp.rename(tmp_dir + '/' + video_filename, self._video_filename) # 将视频从临时文件移出, 顺便重命名 remove_dir(tmp_dir) # 删除临时目录 self.upload_succeed_flag = True # 标记上传成功 break except ConnectionResetError as e: if self._settings['ftp']['show_error_detail']: detail = self._video_filename + ' 在上傳過程中網絡被重置, 將重試最多' + str( max_try_num) + '次' + ', 收到異常: ' + str(e) err_print(self._sn, '上傳狀態', detail, status=1) try_counter = try_counter + 1 except TimeoutError as e: if self._settings['ftp']['show_error_detail']: detail = self._video_filename + ' 在上傳過程中超時, 將重試最多' + str( max_try_num) + '次, 收到異常: ' + str(e) err_print(self._sn, '上傳狀態', detail, status=1) try_counter = try_counter + 1 except socket.timeout as e: if self._settings['ftp']['show_error_detail']: detail = self._video_filename + ' 在上傳過程socket超時, 將重試最多' + str( max_try_num) + '次, 收到異常: ' + str(e) err_print(self._sn, '上傳狀態', detail, status=1) try_counter = try_counter + 1 if not self.upload_succeed_flag: err_print(self._sn, '上傳失敗', self._video_filename + ' 放棄上傳!', status=1) exit_ftp() return self.upload_succeed_flag err_print(self._sn, '上傳完成', self._video_filename, status=2) exit_ftp() # 登出 FTP return self.upload_succeed_flag
def download(self, resolution='', save_dir='', bangumi_tag='', realtime_show_file_size=False, rename='', classify=True): self.realtime_show_file_size = realtime_show_file_size if not resolution: resolution = self._settings['download_resolution'] if save_dir: self._bangumi_dir = save_dir # 用于 cui 用户指定下载在当前目录 if rename: # 如果设定重命名了番剧 self._title = self._title.replace(self._bangumi_name, rename) self._bangumi_name = rename try: self.__get_m3u8_dict() # 获取 m3u8 列表 except TryTooManyTimeError: # 如果在获取 m3u8 过程中发生意外, 则取消此次下载 err_print(self._sn, '下載狀態', '獲取 m3u8 失敗!', status=1) self.video_size = 0 return check_ffmpeg = subprocess.Popen('ffmpeg -h', shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) if check_ffmpeg.stdout.readlines(): # 查找 ffmpeg 是否已放入系统 path self._ffmpeg_path = 'ffmpeg' else: # print('没有在系统PATH中发现ffmpeg,尝试在所在目录寻找') if 'Windows' in platform.system(): self._ffmpeg_path = os.path.join(self._working_dir, 'ffmpeg.exe') else: self._ffmpeg_path = os.path.join(self._working_dir, 'ffmpeg') if not os.path.exists(self._ffmpeg_path): err_print(0, '本項目依賴於ffmpeg, 但ffmpeg未找到', status=1, no_sn=True) raise FileNotFoundError # 如果本地目录下也没有找到 ffmpeg 则丢出异常 # 创建存放番剧的目录,去除非法字符 if bangumi_tag: # 如果指定了番剧分类 self._bangumi_dir = os.path.join( self._bangumi_dir, Config.legalize_filename(bangumi_tag)) if classify: # 控制是否建立番剧文件夹 self._bangumi_dir = os.path.join( self._bangumi_dir, Config.legalize_filename(self._bangumi_name)) if not os.path.exists(self._bangumi_dir): try: os.makedirs(self._bangumi_dir) # 按番剧创建文件夹分类 except FileExistsError as e: err_print(self._sn, '下載狀態', '慾創建的番劇資料夾已存在 ' + str(e), display=False) if not os.path.exists(self._temp_dir): # 建立临时文件夹 try: os.makedirs(self._temp_dir) except FileExistsError as e: err_print(self._sn, '下載狀態', '慾創建的臨時資料夾已存在 ' + str(e), display=False) # 如果不存在指定清晰度,则选取最近可用清晰度 if resolution not in self._m3u8_dict.keys(): if self._settings['lock_resolution']: # 如果用户设定锁定清晰度, 則下載取消 err_msg_detail = '指定清晰度不存在, 因當前鎖定了清晰度, 下載取消. 可用的清晰度: ' + 'P '.join( self._m3u8_dict.keys()) + 'P' err_print(self._sn, '任務狀態', err_msg_detail, status=1) return resolution_list = map(lambda x: int(x), self._m3u8_dict.keys()) resolution_list = list(resolution_list) flag = 9999 closest_resolution = 0 for i in resolution_list: a = abs(int(resolution) - i) if a < flag: flag = a closest_resolution = i # resolution_list.sort() # resolution = str(resolution_list[-1]) # 选取最高可用清晰度 resolution = str(closest_resolution) err_msg_detail = '指定清晰度不存在, 選取最近可用清晰度: ' + resolution + 'P' err_print(self._sn, '任務狀態', err_msg_detail, status=1) self.video_resolution = int(resolution) if self._settings['segment_download_mode']: self.__segment_download_mode(resolution) else: self.__ffmpeg_download_mode(resolution)
def __ffmpeg_download_mode(self, resolution=''): # 设定文件存放路径 filename = self.__get_filename(resolution) downloading_filename = self.__get_temp_filename( resolution, temp_suffix='DOWNLOADING') output_file = os.path.join(self._bangumi_dir, filename) # 完整输出路径 downloading_file = os.path.join(self._temp_dir, downloading_filename) # 构造 ffmpeg 命令 ffmpeg_cmd = [ self._ffmpeg_path, '-user_agent', self._settings['ua'], '-headers', "Origin: https://ani.gamer.com.tw", '-i', self._m3u8_dict[resolution], '-c', 'copy', downloading_file, '-y' ] if os.path.exists(downloading_file): os.remove(downloading_file) # 清理任务失败的尸体 # subprocess.call(ffmpeg_cmd, creationflags=0x08000000) # 仅windows run_ffmpeg = subprocess.Popen(ffmpeg_cmd, stdout=subprocess.PIPE, bufsize=204800, stderr=subprocess.PIPE) def check_ffmpeg_alive(): # 应对ffmpeg卡死, 资源限速等,若 1min 中内文件大小没有增加超过 3M, 则判定卡死 if self.realtime_show_file_size: # 是否实时显示文件大小, 设计仅 cui 下载单个文件或线程数=1时适用 sys.stdout.write('正在下載: sn=' + str(self._sn) + ' ' + filename) sys.stdout.flush() else: err_print(self._sn, '正在下載', filename + ' title=' + self._title) time.sleep(2) time_counter = 1 pre_temp_file_size = 0 while run_ffmpeg.poll() is None: if self.realtime_show_file_size: # 实时显示文件大小 if os.path.exists(downloading_file): size = os.path.getsize(downloading_file) size = size / float(1024 * 1024) size = round(size, 2) sys.stdout.write('\r正在下載: sn=' + str(self._sn) + ' ' + filename + ' ' + str(size) + 'MB ') sys.stdout.flush() else: sys.stdout.write('\r正在下載: sn=' + str(self._sn) + ' ' + filename + ' 文件尚未生成 ') sys.stdout.flush() if time_counter % 60 == 0 and os.path.exists(downloading_file): temp_file_size = os.path.getsize(downloading_file) a = temp_file_size - pre_temp_file_size if a < (3 * 1024 * 1024): err_msg_detail = downloading_filename + ' 在一分钟内仅增加' + str( int(a / float(1024))) + 'KB 判定为卡死, 任务失败!' err_print(self._sn, '下載失败', err_msg_detail, status=1) run_ffmpeg.kill() return pre_temp_file_size = temp_file_size time.sleep(1) time_counter = time_counter + 1 ffmpeg_checker = threading.Thread(target=check_ffmpeg_alive) # 检查线程 ffmpeg_checker.setDaemon(True) # 如果 Anime 线程被 kill, 检查进程也应该结束 ffmpeg_checker.start() run = run_ffmpeg.communicate() return_str = str(run[1]) if self.realtime_show_file_size: sys.stdout.write('\n') sys.stdout.flush() if run_ffmpeg.returncode == 0 and ( return_str.find('Failed to open segment') < 0): # 执行成功 (ffmpeg正常结束, 每个分段都成功下载) if os.path.exists(output_file): os.remove(output_file) # 记录文件大小,单位为 MB self.video_size = int( os.path.getsize(downloading_file) / float(1024 * 1024)) err_print( self._sn, '下載狀態', filename + '本集 ' + str(self.video_size) + 'MB, 正在移至番劇目錄……') if self._settings['use_copyfile_method']: shutil.copyfile(downloading_file, output_file) # 适配rclone挂载盘 os.remove(downloading_file) # 刪除临时合并文件 else: shutil.move(downloading_file, output_file) # 此方法在遇到rclone挂载盘时会出错 self.local_video_path = output_file # 记录保存路径, FTP上传用 self._video_filename = filename # 记录文件名, FTP上传用 err_print(self._sn, '下載完成', filename, status=2) else: err_msg_detail = filename + ' ffmpeg_return_code=' + str( run_ffmpeg.returncode) + ' Bad segment=' + str( return_str.find('Failed to open segment')) err_print(self._sn, '下載失败', err_msg_detail, status=1)
def __segment_download_mode(self, resolution=''): # 设定文件存放路径 filename = self.__get_filename(resolution) merging_filename = self.__get_temp_filename(resolution, temp_suffix='MERGING') output_file = os.path.join(self._bangumi_dir, filename) # 完整输出路径 merging_file = os.path.join(self._temp_dir, merging_filename) url_path = os.path.split( self._m3u8_dict[resolution])[0] # 用于构造完整 chunk 链接 temp_dir = os.path.join(self._temp_dir, str(self._sn) + '-downloading-by-aniGamerPlus') # 临时目录以 sn 命令 if not os.path.exists(temp_dir): # 创建临时目录 os.makedirs(temp_dir) m3u8_path = os.path.join(temp_dir, str(self._sn) + '.m3u8') # m3u8 存放位置 m3u8_text = self.__request(self._m3u8_dict[resolution], no_cookies=True).text # 请求 m3u8 文件 with open(m3u8_path, 'w', encoding='utf-8') as f: # 保存 m3u8 文件在本地 f.write(m3u8_text) pass key_uri = re.search(r'.+URI=.+m3u8key.+', m3u8_text).group() # 找到包含 key 的行 key_uri = re.sub(r'.+URI="', '', key_uri)[0:-1] # 把 key 的链接提取出来 m3u8_key_path = os.path.join(temp_dir, 'key.m3u8key') # key 的存放位置 with open(m3u8_key_path, 'wb') as f: # 保存 key f.write(self.__request(key_uri, no_cookies=True).content) chunk_list = re.findall(r'media_b.+ts.+', m3u8_text) # chunk limiter = threading.Semaphore( self._settings['multi_downloading_segment']) # chunk 并发下载限制器 total_chunk_num = len(chunk_list) finished_chunk_counter = 0 failed_flag = False def download_chunk(uri): limiter.acquire() chunk_name = re.findall(r'media_b.+ts', uri)[0] # chunk 文件名 chunk_local_path = os.path.join(temp_dir, chunk_name) # chunk 路径 nonlocal failed_flag try: with open(chunk_local_path, 'wb') as f: f.write( self.__request(uri, no_cookies=True, show_fail=False, max_retry=8).content) except TryTooManyTimeError: failed_flag = True err_print(self._sn, '下載狀態', 'Bad segment=' + chunk_name, status=1) limiter.release() sys.exit(1) except BaseException as e: failed_flag = True err_print(self._sn, '下載狀態', 'Bad segment=' + chunk_name + ' 發生未知錯誤: ' + str(e), status=1) limiter.release() sys.exit(1) if self.realtime_show_file_size: # 显示完成百分比 nonlocal finished_chunk_counter finished_chunk_counter = finished_chunk_counter + 1 progress_rate = float(finished_chunk_counter / total_chunk_num * 100) progress_rate = round(progress_rate, 2) sys.stdout.write('\r正在下載: sn=' + str(self._sn) + ' ' + filename + ' ' + str(progress_rate) + '% ') sys.stdout.flush() limiter.release() if self.realtime_show_file_size: # 是否实时显示文件大小, 设计仅 cui 下载单个文件或线程数=1时适用 sys.stdout.write('正在下載: sn=' + str(self._sn) + ' ' + filename) sys.stdout.flush() else: err_print(self._sn, '正在下載', filename + ' title=' + self._title) chunk_tasks_list = [] for chunk in chunk_list: chunk_uri = url_path + '/' + chunk task = threading.Thread(target=download_chunk, args=(chunk_uri, )) chunk_tasks_list.append(task) task.setDaemon(True) task.start() for task in chunk_tasks_list: # 等待所有任务完成 while True: if failed_flag: err_print(self._sn, '下載失败', filename, status=1) self.video_size = 0 return if task.isAlive(): time.sleep(1) else: break # m3u8 本地化 # replace('\\', '\\\\') 为转义win路径 m3u8_text_local_version = m3u8_text.replace( key_uri, os.path.join(temp_dir, 'key.m3u8key')).replace('\\', '\\\\') for chunk in chunk_list: chunk_filename = re.findall(r'media_b.+ts', chunk)[0] # chunk 文件名 chunk_path = os.path.join(temp_dir, chunk_filename).replace( '\\', '\\\\') # chunk 本地路径 m3u8_text_local_version = m3u8_text_local_version.replace( chunk, chunk_path) with open(m3u8_path, 'w', encoding='utf-8') as f: # 保存本地化的 m3u8 f.write(m3u8_text_local_version) if self.realtime_show_file_size: sys.stdout.write('\n') sys.stdout.flush() err_print(self._sn, '下載狀態', filename + ' 下載完成, 正在解密合并……') # 构造 ffmpeg 命令 ffmpeg_cmd = [ self._ffmpeg_path, '-allowed_extensions', 'ALL', '-i', m3u8_path, '-c', 'copy', merging_file, '-y' ] # 执行 ffmpeg run_ffmpeg = subprocess.Popen(ffmpeg_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) run_ffmpeg.communicate() # 记录文件大小,单位为 MB self.video_size = int( os.path.getsize(merging_file) / float(1024 * 1024)) # 重命名 err_print( self._sn, '下載狀態', filename + ' 解密合并完成, 本集 ' + str(self.video_size) + 'MB, 正在移至番劇目錄……') if os.path.exists(output_file): os.remove(output_file) if self._settings['use_copyfile_method']: shutil.copyfile(merging_file, output_file) # 适配rclone挂载盘 os.remove(merging_file) # 刪除临时合并文件 else: shutil.move(merging_file, output_file) # 此方法在遇到rclone挂载盘时会出错 # 删除临时目录 shutil.rmtree(temp_dir) self.local_video_path = output_file # 记录保存路径, FTP上传用 self._video_filename = filename # 记录文件名, FTP上传用 err_print(self._sn, '下載完成', filename, status=2)
def __get_m3u8_dict(self): # m3u8获取模块参考自 https://github.com/c0re100/BahamutAnimeDownloader def get_device_id(): req = 'https://ani.gamer.com.tw/ajax/getdeviceid.php' f = self.__request(req) self._device_id = f.json()['deviceid'] return self._device_id def get_playlist(): req = 'https://ani.gamer.com.tw/ajax/m3u8.php?sn=' + str( self._sn) + '&device=' + self._device_id f = self.__request(req) self._playlist = f.json() def random_string(num): chars = 'abcdefghijklmnopqrstuvwxyz0123456789' random.seed(int(round(time.time() * 1000))) result = [] for i in range(num): result.append(chars[random.randint(0, len(chars) - 1)]) return ''.join(result) def gain_access(): req = 'https://ani.gamer.com.tw/ajax/token.php?adID=0&sn=' + str( self._sn ) + "&device=" + self._device_id + "&hash=" + random_string(12) # 返回基础信息, 用于判断是不是VIP return self.__request(req).json() def unlock(): req = 'https://ani.gamer.com.tw/ajax/unlock.php?sn=' + str( self._sn) + "&ttl=0" f = self.__request(req) # 无响应正文 def check_lock(): req = 'https://ani.gamer.com.tw/ajax/checklock.php?device=' + self._device_id + '&sn=' + str( self._sn) f = self.__request(req) def start_ad(): req = "https://ani.gamer.com.tw/ajax/videoCastcishu.php?sn=" + str( self._sn) + "&s=194699" f = self.__request(req) # 无响应正文 def skip_ad(): req = "https://ani.gamer.com.tw/ajax/videoCastcishu.php?sn=" + str( self._sn) + "&s=194699&ad=end" f = self.__request(req) # 无响应正文 def video_start(): req = "https://ani.gamer.com.tw/ajax/videoStart.php?sn=" + str( self._sn) f = self.__request(req) def check_no_ad(): req = "https://ani.gamer.com.tw/ajax/token.php?sn=" + str( self._sn ) + "&device=" + self._device_id + "&hash=" + random_string(12) f = self.__request(req) resp = f.json() if 'time' in resp.keys(): if resp['time'] == 1: # print('check_no_ad: Adaway!') pass else: err_print(self._sn, 'check_no_ad: Ads not away?', status=1) else: # print('check_no_ad: Not in right area.') err_print(self._sn, '遭到動畫瘋地區限制, 你的IP可能不被動畫瘋認可!', status=1) sys.exit(1) def parse_playlist(): req = 'https:' + self._playlist['src'] f = self.__request(req, no_cookies=True) url_prefix = re.sub(r'playlist.+', '', self._playlist['src']) # m3u8 URL 前缀 m3u8_list = re.findall(r'=\d+x\d+\n.+', f.content.decode()) # 将包含分辨率和 m3u8 文件提取 m3u8_dict = {} for i in m3u8_list: key = re.findall(r'=\d+x\d+', i)[0] # 提取分辨率 key = re.findall(r'x\d+', key)[0][1:] # 提取纵向像素数,作为 key value = re.findall(r'chunklist.+', i)[0] # 提取 m3u8 文件 value = 'https:' + url_prefix + value # 组成完整的 m3u8 URL m3u8_dict[key] = value self._m3u8_dict = m3u8_dict get_device_id() user_info = gain_access() unlock() check_lock() unlock() unlock() # 收到錯誤反饋 # 可能是限制級動畫要求登陸 if 'error' in user_info.keys(): msg = '《' + self._title + '》 ' msg = msg + 'code=' + str( user_info['error'] ['code']) + ' message: ' + user_info['error']['message'] err_print(self._sn, '收到錯誤', msg, status=1) sys.exit(1) if not user_info['vip']: # 如果用户不是 VIP, 那么等待广告(8s) err_print(self._sn, '正在等待', '《' + self.get_title() + '》 由於不是VIP賬戶, 正在等待8s廣告時間') start_ad() time.sleep(8) skip_ad() else: err_print(self._sn, '開始下載', '《' + self.get_title() + '》 識別到VIP賬戶, 立即下載') video_start() check_no_ad() get_playlist() parse_playlist()
def __request(self, req, no_cookies=False, show_fail=True, max_retry=3): # 获取页面 error_cnt = 0 while True: try: if self._cookies and not no_cookies: f = self._session.get(req, headers=self._req_header, cookies=self._cookies, timeout=10) else: f = self._session.get(req, headers=self._req_header, cookies={}, timeout=10) except requests.exceptions.RequestException as e: if error_cnt >= max_retry: raise TryTooManyTimeError('任務狀態: sn=' + str(self._sn) + ' 请求失败次数过多!请求链接:\n%s' % req) err_detail = 'ERROR: 请求失败!except:\n' + str( e) + '\n3s后重试(最多重试' + str(max_retry) + '次)' if show_fail: err_print(self._sn, '任務狀態', err_detail) time.sleep(3) error_cnt += 1 else: break # 处理 cookie if not self._cookies: # 当实例中尚无 cookie, 则读取 self._cookies = f.cookies.get_dict() elif 'nologinuser' not in self._cookies.keys( ) and 'BAHAID' not in self._cookies.keys(): # 处理游客cookie if 'nologinuser' in f.cookies.get_dict().keys(): # self._cookies['nologinuser'] = f.cookies.get_dict()['nologinuser'] self._cookies = f.cookies.get_dict() else: # 如果用户提供了 cookie, 则处理cookie刷新 if 'set-cookie' in f.headers.keys(): # 发现server响应了set-cookie if 'deleted' in f.headers.get('set-cookie'): # set-cookie刷新cookie只有一次机会, 如果其他线程先收到, 则此处会返回 deleted # 等待其他线程刷新了cookie, 重新读入cookie err_print(self._sn, '收到cookie重置響應', display=False) time.sleep(2) try_counter = 0 succeed_flag = False while try_counter < 3: # 尝试读三次, 不行就算了 old_BAHARUNE = self._cookies['BAHARUNE'] self._cookies = Config.read_cookie() err_print(self._sn, '讀取cookie', 'cookie.txt最後修改時間: ' + Config.get_cookie_time() + ' 第' + str(try_counter) + '次嘗試', display=False) if old_BAHARUNE != self._cookies['BAHARUNE']: # 新cookie读取成功 succeed_flag = True err_print(self._sn, '讀取cookie', '新cookie讀取成功', display=False) break else: err_print(self._sn, '讀取cookie', '新cookie讀取失敗', display=False) random_wait_time = random.uniform(2, 5) time.sleep(random_wait_time) try_counter = try_counter + 1 if not succeed_flag: self._cookies = {} err_print(0, '用戶cookie更新失敗! 使用游客身份訪問', status=1, no_sn=True) Config.invalid_cookie() # 将失效cookie更名 elif '__cfduid' in f.headers.get( 'set-cookie') and 'BAHARUNE' not in f.headers.get( 'set-cookie'): # cookie 刷新两步走, 这是第二步, 追加在第一步后面 # 此时self._cookies已是完整新cookie,不需要再从文件载入 # 20190507 发现是一步到位了, 但保不准会不会该回去, 姑且加个 and self._cookies['__cfduid'] = f.cookies.get_dict( )['__cfduid'] Config.renew_cookies(self._cookies) # 保存全新cookie err_print(0, '用戶cookie已更新', status=2, no_sn=True) elif 'hahatoken' in f.headers.get( 'set-cookie') and 'BAHARUNE' not in f.headers.get( 'set-cookie'): # 巴哈cookie升级 # https://github.com/miyouzi/aniGamerPlus/issues/8 # 每次请求都会返回一个token, token生命周期 3000s (即50min) # 这一点都不节能啊! ( self._cookies['hahatoken'] = f.cookies.get_dict( )['hahatoken'] Config.renew_cookies(self._cookies, log=False) else: # 这是第一步 # 本线程收到了新cookie err_print(self._sn, '收到新cookie', display=False) Config.renew_cookies(f.cookies.get_dict()) # 保存一半新cookie self._cookies = Config.read_cookie() # 载入一半新cookie self.__request('https://ani.gamer.com.tw/' ) # 马上完成cookie刷新第二步, 以免正好在刚要解析m3u8时掉链子 return f
def worker(sn, sn_info, realtime_show_file_size=False): bangumi_tag = sn_info['tag'] rename = sn_info['rename'] def upload_quit(): queue.pop(sn) processing_queue.remove(sn) upload_limiter.release() # 并发上传限制器 sys.exit(0) anime_in_db = read_db(sn) # 如果用户设定要上传且已经下载好了但还没有上传成功, 那么仅上传 if settings['upload_to_server'] and anime_in_db[ 'status'] == 1 and anime_in_db['remote_status'] == 0: upload_limiter.acquire() # 并发上传限制器 anime = build_anime(sn) if anime['failed']: err_print(sn, '任务失敗', '從任務列隊中移除, 等待下次更新重試.', status=1) upload_quit() # 视频信息抓取成功 anime = anime['anime'] if not os.path.exists(anime_in_db['local_file_path']): # 如果数据库中记录的文件路径已失效 update_db(anime) err_msg_detail = 'title=\"' + anime.get_title( ) + '\" 本地文件丢失, 從任務列隊中移除, 等待下次更新重試.' err_print(sn, '上传失敗', err_msg_detail, status=1) upload_quit() anime.local_video_path = anime_in_db['local_file_path'] # 告知文件位置 anime.video_size = anime_in_db['file_size'] # 通過 update_db() 下载状态检查 anime.video_resolution = anime_in_db['resolution'] # 避免更新时把分辨率变成0 try: if not anime.upload(bangumi_tag): # 如果上传失败 err_msg_detail = 'title=\"' + anime.get_title( ) + '\" 從任務列隊中移除, 等待下次更新重試.' err_print(sn, '上传失敗', err_msg_detail, 1) else: update_db(anime) err_print(sn, '任務完成', status=2) except BaseException as e: err_msg_detail = 'title=\"' + anime.get_title( ) + '\" 發生未知錯誤, 等待下次更新重試: ' + str(e) err_print(sn, '上傳失敗', '異常詳情:\n' + traceback.format_exc(), status=1, display=False) err_print(sn, '上傳失敗', err_msg_detail, 1) upload_quit() # =====下载模块 ===== thread_limiter.acquire() # 并发下载限制器 anime = build_anime(sn) if anime['failed']: queue.pop(sn) processing_queue.remove(sn) thread_limiter.release() err_print(sn, '任务失敗', '從任務列隊中移除, 等待下次更新重試.', status=1) sys.exit(1) anime = anime['anime'] try: anime.download(settings['download_resolution'], bangumi_tag=bangumi_tag, rename=rename, realtime_show_file_size=realtime_show_file_size, classify=settings['classify_bangumi']) except BaseException as e: # 兜一下各种奇奇怪怪的错误 err_print(sn, '下載異常', '發生未知錯誤: ' + str(e), status=1) err_print(sn, '下載異常', '異常詳情:\n' + traceback.format_exc(), status=1, display=False) anime.video_size = 0 if anime.video_size < 5: # 下载失败 queue.pop(sn) processing_queue.remove(sn) thread_limiter.release() err_msg_detail = 'title=\"' + anime.get_title( ) + '\" 從任務列隊中移除, 等待下次更新重試.' err_print(sn, '任务失敗', err_msg_detail, status=1) if int(sn) in Config.tasks_progress_rate.keys(): del Config.tasks_progress_rate[int(sn)] # 任务失败, 不在监控此任务进度 sys.exit(1) update_db(anime) # 下载完成后, 更新数据库 thread_limiter.release() # 并发下载限制器 # =====下载模块结束 ===== # =====上传模块===== if settings['upload_to_server']: upload_limiter.acquire() # 并发上传限制器 try: anime.upload(bangumi_tag) # 上传至服务器 except BaseException as e: # 兜一下各种奇奇怪怪的错误 err_print(sn, '上傳異常', '發生未知錯誤, 從任務列隊中移除, 等待下次更新重試: ' + str(e), status=1) err_print(sn, '上傳異常', '異常詳情:\n' + traceback.format_exc(), status=1, display=False) upload_quit() update_db(anime) # 上传完成后, 更新数据库 upload_limiter.release() # 并发上传限制器 # =====上传模块结束===== queue.pop(sn) # 从任务列队中移除 processing_queue.remove(sn) # 从当前任务列队中移除 err_print(sn, '任務完成', status=2)
def __cui(sn, cui_resolution, cui_download_mode, cui_thread_limit, ep_range, cui_save_dir='', classify=True, get_info=False, user_cmd=False, realtime_show=True, cui_danmu=False): global thread_limiter thread_limiter = threading.Semaphore(cui_thread_limit) global danmu danmu = cui_danmu if realtime_show: if cui_thread_limit == 1 or cui_download_mode in ('single', 'latest', 'largest-sn'): realtime_show_file_size = True else: realtime_show_file_size = False else: realtime_show_file_size = False if cui_download_mode == 'single': if get_info: print('當前模式: 查詢本集資訊\n') else: print('當前下載模式: 僅下載本集\n') if get_info: __get_info_only(sn) else: __download_only(sn, cui_resolution, cui_save_dir, realtime_show_file_size=realtime_show_file_size, classify=classify) elif cui_download_mode == 'latest' or cui_download_mode == 'largest-sn': if cui_download_mode == 'latest': if get_info: print('當前模式: 查詢本番劇最後一集資訊\n') else: print('當前下載模式: 下載本番劇最後一集\n') else: if get_info: print('當前模式: 查詢本番劇最近上傳一集資訊\n') else: print('當前下載模式: 下載本番劇最近上傳的一集\n') anime = build_anime(sn) if anime['failed']: sys.exit(1) anime = anime['anime'] bangumi_list = list(anime.get_episode_list().values()) if cui_download_mode == 'largest-sn': bangumi_list.sort() if get_info: __get_info_only(bangumi_list[-1]) else: __download_only(bangumi_list[-1], cui_resolution, cui_save_dir, realtime_show_file_size=realtime_show_file_size, classify=classify) elif cui_download_mode == 'all': if get_info: print('當前模式: 查詢本番劇所有劇集資訊\n') else: print('當前下載模式: 下載本番劇所有劇集\n') anime = build_anime(sn) if anime['failed']: sys.exit(1) anime = anime['anime'] bangumi_list = list(anime.get_episode_list().values()) bangumi_list.sort() tasks_counter = 0 # 任务计数器 for anime_sn in bangumi_list: if get_info: task = threading.Thread(target=__get_info_only, args=(anime_sn, )) else: task = threading.Thread( target=__download_only, args=(anime_sn, cui_resolution, cui_save_dir, realtime_show_file_size, classify)) task.setDaemon(True) thread_tasks.append(task) task.start() tasks_counter = tasks_counter + 1 print('添加任务列隊: sn=' + str(anime_sn)) if get_info: print('所有查詢任務已添加至列隊, 共 ' + str(tasks_counter) + ' 個任務\n') else: print('所有下載任務已添加至列隊, 共 ' + str(tasks_counter) + ' 個任務, ' + '執行緒數: ' + str(cui_thread_limit) + '\n') elif cui_download_mode == 'range': if get_info: print('當前模式: 查詢本番劇指定劇集資訊\n') else: print('當前下載模式: 下載本番劇指定劇集\n') anime = build_anime(sn) if anime['failed']: sys.exit(1) anime = anime['anime'] episode_dict = anime.get_episode_list() bangumi_ep_list = list(episode_dict.keys()) # 本番剧集列表 tasks_counter = 0 # 任务计数器 for ep in ep_range: if ep in bangumi_ep_list: if get_info: a = threading.Thread(target=__get_info_only, args=(episode_dict[ep], )) else: a = threading.Thread(target=__download_only, args=(episode_dict[ep], cui_resolution, cui_save_dir, realtime_show_file_size)) a.setDaemon(True) thread_tasks.append(a) a.start() tasks_counter = tasks_counter + 1 if get_info: print('添加查詢列隊: sn=' + str(episode_dict[ep]) + ' 《' + anime.get_bangumi_name() + '》 第 ' + ep + ' 集') else: print('添加任务列隊: sn=' + str(episode_dict[ep]) + ' 《' + anime.get_bangumi_name() + '》 第 ' + ep + ' 集') else: err_print(0, '《' + anime.get_bangumi_name() + '》 第 ' + ep + ' 集不存在!', status=1, no_sn=True) print('所有任務已添加至列隊, 共 ' + str(tasks_counter) + ' 個任務, ' + '執行緒數: ' + str(cui_thread_limit) + '\n') elif cui_download_mode == 'sn-range': if get_info: print('當前模式: 查詢本番劇指定sn範圍資訊\n') else: print('當前下載模式: 下載本番劇指定sn範圍劇集\n') anime = build_anime(sn) if anime['failed']: sys.exit(1) anime = anime['anime'] # 剧集列表 key value 互换, {'sn', '剧集名'} episode_dict = { value: key for key, value in anime.get_episode_list().items() } ep_sn_list = list(episode_dict.keys()) # 本番剧集sn列表 tasks_counter = 0 # 任务计数器 ep_range = list(map(lambda x: int(x), ep_range)) for sn in ep_sn_list: if sn in ep_range: # 如果该 sn 在用户指定的 sn 范围里 if get_info: a = threading.Thread(target=__get_info_only, args=(sn, )) else: a = threading.Thread(target=__download_only, args=(sn, cui_resolution, cui_save_dir, realtime_show_file_size)) a.setDaemon(True) thread_tasks.append(a) a.start() tasks_counter = tasks_counter + 1 if get_info: print('添加查詢列隊: sn=' + str(sn) + ' 《' + anime.get_bangumi_name() + '》 第 ' + episode_dict[sn] + ' 集') else: print('添加任务列隊: sn=' + str(sn) + ' 《' + anime.get_bangumi_name() + '》 第 ' + episode_dict[sn] + ' 集') print('所有任務已添加至列隊, 共 ' + str(tasks_counter) + ' 個任務, ' + '執行緒數: ' + str(cui_thread_limit) + '\n') elif cui_download_mode == 'multi': if get_info: print('當前模式: 查詢指定sn資訊\n') else: print('當前下載模式: 下載指定sn劇集\n') tasks_counter = 0 for sn in ep_range: if get_info: a = threading.Thread(target=__get_info_only, args=(sn, )) else: a = threading.Thread(target=__download_only, args=(sn, cui_resolution, cui_save_dir, realtime_show_file_size)) a.setDaemon(True) thread_tasks.append(a) a.start() tasks_counter = tasks_counter + 1 print('所有任務已添加至列隊, 共 ' + str(tasks_counter) + ' 個任務, ' + '執行緒數: ' + str(cui_thread_limit) + '\n') elif cui_download_mode in ('list', 'sn-list'): if get_info: # 如果為list模式也仅查询名单中的sn信息, 可用于检查sn是否输入正确 print('當前模式: 查詢sn_list.txt中指定sn的資訊\n') ep_range = Config.read_sn_list().keys() for sn in ep_range: anime = build_anime(sn) if anime['failed']: sys.exit(1) anime = anime['anime'] anime.get_info() else: if cui_download_mode == 'sn-list': print('當前下載模式: 下載sn_list.txt中指定的sn劇集\n') for i in sn_dict: sn_dict[i]['mode'] = 'single' else: print('當前下載模式: 單次下載sn_list.txt中的番劇\n') check_tasks() # 检查更新,生成任务列队 for sn in queue.keys(): # 遍历任务列队 processing_queue.append(sn) task = threading.Thread(target=worker, args=(sn, queue[sn], realtime_show_file_size)) task.setDaemon(True) thread_tasks.append(task) task.start() err_print(sn, '加入任务列隊') msg = '共 ' + str(len(queue)) + ' 個任務' err_print(0, '任務資訊', msg, no_sn=True) print() __kill_thread_when_ctrl_c() kill_gost() # 结束 gost # 结束后执行用户自定义命令 if user_cmd: print() os.popen(settings['user_command']) err_print(0, '任務完成', '已執行用戶命令', no_sn=True, status=2) sys.exit(0)
def user_exit(signum, frame): err_print(0, '你終止了程序!', '\n', status=1, no_sn=True, prefix='\n\n') kill_gost() # 结束 gost sys.exit(255)