Exemple #1
0
 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)
Exemple #3
0
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"}'
Exemple #4
0
 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)
Exemple #5
0
 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)
Exemple #6
0
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
Exemple #7
0
 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)
Exemple #8
0
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]
Exemple #9
0
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協議')
Exemple #11
0
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)
Exemple #12
0
        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()
Exemple #13
0
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()
Exemple #14
0
        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
Exemple #15
0
 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)
Exemple #16
0
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"}'
Exemple #17
0
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)
Exemple #18
0
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()
Exemple #19
0
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()
Exemple #21
0
        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
Exemple #22
0
    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
Exemple #23
0
    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)
Exemple #24
0
    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)
Exemple #25
0
    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)
Exemple #26
0
    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()
Exemple #27
0
    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)