def start(self, glossary, save_dir, chunk_size=1024 * 50): """ :param dict[str, dict[str, int | str]] | list[str] glossary: :param str | Path save_dir: :param int chunk_size: 一度にサーバーに要求するファイルサイズ :rtype: bool """ utils.check_arg(locals()) self.logger.debug("Directory to save in: %s", save_dir) self.logger.debug("Dictionary of Videos: %s", glossary) self.save_dir = utils.make_dir(save_dir) if isinstance(glossary, list): glossary = get_infos(glossary, self.logger) self.glossary = glossary self.logger.info(Msg.nd_start_dl_video, len(self.glossary), list(self.glossary)) for index, video_id in enumerate(self.glossary.keys()): self.logger.info(Msg.nd_download_video, index + 1, len(glossary), video_id, self.glossary[video_id][KeyGTI.TITLE]) self._download(video_id, chunk_size) if len(glossary) > 1: time.sleep(1) return True
def fetch_meta(self, with_header=True): """ マイリストのメタ情報を表示する。 :param bool with_header: :rtype: list[list[str]] """ utils.check_arg(locals()) self.logger.info(Msg.ml_loading_mylists) counts = len( json.loads(self.session.get(URL.URL_ListDef).text)["mylistitem"]) if with_header: container = [["ID", "名前", "項目数", "状態", "作成日", "説明文"]] else: container = [] # とりあえずマイリストのデータ container.append( [utils.DEFAULT_ID, utils.DEFAULT_NAME, counts, "非公開", "--", ""]) # その他のマイリストのデータ # 作成日順に並び替えてから情報を得る for item in sorted(self.mylists.values(), key=lambda this: this["since"]): response = self.session.get(URL.URL_ListOne, params={ "group_id": item["id"] }).text counts = len(json.loads(response)["mylistitem"]) container.append([ item[MKey.ID], item[MKey.NAME], counts, item[MKey.PUBLICITY], item[MKey.SINCE], item[MKey.DESCRIPTION] ]) return container
def purge_mylist(self, list_id, confident=False): """ 指定したマイリストを削除する。 :param int | str list_id: マイリストの名前またはID :param bool confident: :rtype: bool """ utils.check_arg(locals()) if utils.ALL_ITEM == list_id: raise utils.MylistError(Err.cant_perform_all) list_id, list_name = self._get_list_id(list_id) if list_id == utils.DEFAULT_ID: raise utils.MylistError(Err.deflist_to_create_or_purge) if not confident and not self._confirmation("purge", list_name): print(Msg.ml_answer_no) return False res = self.get_response("purge", list_id=list_id) if res["status"] != "ok": self.logger.error(Err.failed_to_purge, list_name, res["status"]) return False else: self.logger.info(Msg.ml_done_purge, list_name) del self.mylists[list_id] return True
def _download(self, video_id, chunk_size=1024 * 50): """ :param str video_id: 動画ID (e.g. sm1234) :param int chunk_size: 一度にサーバーに要求するファイルサイズ :rtype: bool """ utils.check_arg(locals()) db = self.glossary[video_id] if video_id.startswith("so"): redirected = self.session.get(URL.URL_Watch + video_id).url.split("/")[-1] db[KeyGTI.V_OR_T_ID] = redirected self.logger.debug( "Video ID: %s and its Thread ID (of officials):" " %s", video_id, db[KeyGTI.V_OR_T_ID]) response = utils.get_from_getflv(db[KeyGTI.V_OR_T_ID], self.session, self.logger) vid_url = response[KeyGetFlv.VIDEO_URL] is_premium = response[KeyGetFlv.IS_PREMIUM] # 動画視聴ページに行ってCookieをもらってくる self.session.head(URL.URL_Watch + video_id) # connect timeoutを10秒, read timeoutを30秒に設定 # ↓この時点ではダウンロードは始まらず、ヘッダーだけが来ている video_data = self.session.get(url=vid_url, stream=True, timeout=(10.0, 30.0)) db[KeyGTI.FILE_SIZE] = int(video_data.headers["content-length"]) self.logger.debug("File Size: %s (Premium: %s)", db[KeyGTI.FILE_SIZE], [False, True][int(is_premium)]) return self._saver(video_id, video_data, chunk_size)
def _get_list_id(self, search_for): """ 指定されたIDまたは名前を持つマイリストのIDを得る。 :param int | str search_for: マイリスト名またはマイリストID :rtype: (int, str) """ utils.check_arg(locals()) result = self.get_list_id(search_for) if result.get("error") is True: if result.get("err_dic"): # 同じ名前のマイリストが複数あったとき self.logger.error(result.get("err_msg")) for single in result.get("err_dic").values(): print(Err.name_ambiguous_detail.format(**single), file=sys.stderr) sys.exit() else: # 存在しなかったとき raise MylistNotFoundError(result.get("err_msg")) else: list_id = result["list_id"] list_name = result["list_name"] self.logger.debug("List_id: %s, List_name: %s", list_id, list_name) return list_id, list_name
def create_mylist(self, mylist_name, is_public=False, description=""): """ mylist_name を名前に持つマイリストを作る。 :param str mylist_name: マイリストの名前 :param bool is_public: True なら公開マイリストになる :param str description: マイリストの説明文 :rtype: bool """ utils.check_arg(locals()) if utils.ALL_ITEM == mylist_name: raise utils.MylistError(Err.cant_perform_all) if mylist_name == "" or mylist_name == utils.DEFAULT_NAME: raise utils.MylistError(Err.cant_create) res = self.get_response("create", is_public=is_public, mylist_name=mylist_name, description=description) if res["status"] != "ok": self.logger.error(Err.failed_to_create, mylist_name, res) return False else: self.mylists = self.get_mylists_info() item = self.mylists[res[MKey.ID]] self.logger.info(Msg.ml_done_create, res[MKey.ID], item[MKey.NAME], item[MKey.PUBLICITY], item[MKey.DESCRIPTION]) if mylist_name != item[MKey.NAME]: self.logger.info(Err.name_replaced, mylist_name, item[MKey.NAME]) return True
def show(self, list_id, file_name=None, table=False, survey=False): """ そのマイリストに登録された動画を一覧する。 :param int | str list_id: マイリストの名前またはID。0で「とりあえずマイリスト」。 :param str | Path | None file_name: ファイル名。ここにリストを書き出す。 :param bool table: Trueで表形式で出力する。 :param bool survey: Trueで全てのマイリストの情報をまとめて出力する。 :rtype: str """ utils.check_arg({"list_id": list_id, "table": table, "survey": survey}) if file_name: file_name = utils.make_dir(file_name) if table: # 表形式の場合 if list_id == utils.ALL_ITEM: if survey: cont = self._construct_table(self.fetch_all()) else: cont = self._construct_table(self.fetch_meta()) else: cont = self._construct_table(self.fetch_one(list_id)) else: # タブ区切りテキストの場合 if list_id == utils.ALL_ITEM: if survey: cont = self._construct_tsv(self.fetch_all()) else: cont = self._construct_tsv(self.fetch_meta()) else: cont = self._construct_tsv(self.fetch_one(list_id)) return self._writer(cont, file_name)
def _download(self, video_id, xml=False): """ :param str video_id: 動画ID (e.g. sm1234) :param bool xml: :rtype: bool """ utils.check_arg(locals()) db = self.glossary[video_id] if video_id.startswith("so"): redirected = self.session.get(URL.URL_Watch + video_id).url.split("/")[-1] db[KeyGTI.V_OR_T_ID] = redirected self.logger.debug( "Video ID: %s and its Thread ID (of officials):" " %s", video_id, db[KeyGTI.V_OR_T_ID]) response = utils.get_from_getflv(db[KeyGTI.V_OR_T_ID], self.session, self.logger) if response is None: time.sleep(4) print(Err.waiting_for_permission) time.sleep(4) return self._download(video_id, xml) thread_id = response[KeyGetFlv.THREAD_ID] msg_server = response[KeyGetFlv.MSG_SERVER] user_id = response[KeyGetFlv.USER_ID] user_key = response[KeyGetFlv.USER_KEY] opt_thread_id = response[KeyGetFlv.OPT_THREAD_ID] needs_key = response[KeyGetFlv.NEEDS_KEY] if xml and video_id.startswith(("sm", "nm")): req_param = self.make_param_xml(thread_id, user_id) self.logger.debug("Posting Parameters: %s", req_param) res_com = self.session.post(url=msg_server, data=req_param) comment_data = res_com.text.replace("><", ">\n<") else: if video_id.startswith(("sm", "nm")): req_param = self.make_param_json(False, user_id, user_key, thread_id) else: thread_key, force_184 = self.get_thread_key( db[KeyGTI.V_OR_T_ID], needs_key) req_param = self.make_param_json(True, user_id, user_key, thread_id, opt_thread_id, thread_key, force_184) self.logger.debug("Posting Parameters: %s", req_param) res_com = self.session.post(url=URL.URL_Msg_JSON, json=req_param) comment_data = res_com.text.replace("}, ", "},\n") comment_data = comment_data.encode(res_com.encoding).decode("utf-8") return self._saver(video_id, comment_data, xml)
def _download(self, video_id, is_large=True): """ :param str video_id: 動画ID (e.g. sm1234) :param bool is_large: 大きいサムネイルを取りに行くかどうか :rtype: bool """ utils.check_arg(locals()) url = self.glossary[video_id][KeyGTI.THUMBNAIL_URL] if is_large: url += ".L" image_data = self._worker(video_id, url, is_large) if not image_data: return False return self._saver(video_id, image_data)
def _construct_id(cls, container): """ IDだけを出力する。 :param list[list[str]] container: 表示したい動画IDのリスト。 :rtype: str """ utils.check_arg(locals()) if len(container) == 0: return "" else: return "\n".join([ str(item[0]) for item in container if item is not None and len(item) > 0 ])
def _construct_tsv(cls, container): """ TSV形式で出力する。 :param list[list[str]] container: 表示したい内容を含むリスト。 :rtype: str """ utils.check_arg(locals()) if len(container) == 0: return "" else: first = container.pop(0) rows = [[str(item) for item in row] for row in container] rows.insert(0, first) return "\n".join(["\t".join(row) for row in rows])
def _construct_id_name(cls, container): """ 動画IDやマイリストIDとその名前だけを出力する。 :param list[list[str]] container: 表示したいIDの入ったリスト。 :rtype: str """ utils.check_arg(locals()) if len(container) == 0: return "" else: return "\n".join([ "{}\t{}".format(item[0], item[1]) for item in container if item is not None and len(item) > 0 ])
def get_title(self, video_id): """ getthumbinfo APIから、タイトルをもらってくる :param str video_id: 動画ID :rtype:str """ utils.check_arg(locals()) document = ElementTree.fromstring( self.session.get(URL.URL_Info + video_id).text) # 「status="ok"」 なら動画は生存 / 存在しない動画には「status="fail"」が返る if not document.get("status").lower() == "ok": self.logger.error(Msg.nd_deleted_or_private, video_id) return "" else: return html.unescape(document[0].find("title").text)
def get_item_ids(self, list_id, *videoids): """ そのマイリストに含まれている item_id の一覧を返す。 全て、あるいは指定した(中での生存している)動画の Item IDを返す。 item_id は sm1234 などの動画IDとは異なるもので、 マイリスト間の移動や複製に必要となる。 :param int | str list_id: マイリストの名前またはID :param list[str] | tuple[str] videoids: :rtype: dict[str, str] """ utils.check_arg(locals()) list_id, _ = self._get_list_id(list_id) # *videoids が要素数1のタプル ("*") or # *videoids が要素数0のタプル(即ち未指定) -> 全体モード # 何かしら指定されているなら -> 個別モード if len(videoids) == 0 or (len(videoids) == 1 and utils.ALL_ITEM in videoids): whole = True else: whole = False self.logger.debug("Is in whole mode?: %s", whole) if list_id == utils.DEFAULT_ID: jtext = json.loads(self.session.get(URL.URL_ListDef).text) else: jtext = json.loads( self.session.get(URL.URL_ListOne, params={ "group_id": list_id }).text) self.logger.debug("Response: %s", jtext) results = {} for item in jtext["mylistitem"]: data = item["item_data"] # 0以外のは削除されているか非公開 if not whole: if not "0" == data["deleted"]: self.logger.debug(Msg.ml_deleted_or_private, data) continue if whole or data["video_id"] in videoids: results.update({data["video_id"]: item["item_id"]}) return results
def add(self, list_id, *videoids): """ そのマイリストに、 指定した動画を追加する。 :param int | str list_id: マイリストの名前またはID :param str videoids: 追加する動画ID :rtype: bool """ utils.check_arg(locals()) if utils.ALL_ITEM == list_id or utils.ALL_ITEM in videoids: raise utils.MylistError(Err.cant_perform_all) list_id, list_name = self._get_list_id(list_id) to_def = (list_id == utils.DEFAULT_ID) self.logger.info(Msg.ml_will_add, list_name, list(videoids)) _done = [] for _counter, vd_id in enumerate(videoids): _counter += 1 res = self.get_response("add", to_def=to_def, list_id_to=list_id, video_id=vd_id) try: self._should_continue(res, video_id=vd_id, list_name=list_name, count_now=_counter, count_whole=len(videoids)) self.logger.info(Msg.ml_done_add, _counter, len(videoids), vd_id) _done.append(vd_id) time.sleep(0.5) except MylistAPIError as error: if error.ok: return True else: # エラーが起きた場合 self.logger.error(Err.remaining, [ i for i in videoids if i not in _done and i != utils.ALL_ITEM ]) raise return True
def _construct_table(cls, container): """ Asciiテーブル形式でリストの中身を表示する。 入力の形式は以下の通り: [ ["header1", "header2", "header3"], ["row_1_1", "row_1_2", "row_1_3"], ["row_2_1", "row_2_2", "row_2_3"], ["row_3_1", "row_3_2", "row_3_3"] ] 最後のprintで、ユニコード特有の文字はcp932のコマンドプロンプトでは表示できない。 この対処として幾つかの方法で別の表現に置き換えることができるのだが、例えば「♥」は =================== ================================================== メソッド 変換後 ------------------- -------------------------------------------------- backslashreplace \u2665 xmlcharrefreplace ♥ replace ? =================== ================================================== と表示される。 :param list[list[str]] container: 表示したい内容を含むリスト。 :rtype: str """ utils.check_arg(locals()) if len(container) == 0: return "" elif not PrettyTable: raise ImportError(Err.not_installed, "PrettyTable") else: column_names = container.pop(0) table = PrettyTable(column_names) for column in column_names: table.align[column] = "l" for row in container: table.add_row(row) return table.get_string()
def _writer(self, text, file_name=None): """ ファイルまたは標準出力に書き出す。 :param str text: 内容。 :param str | Path | None file_name: ファイル名またはそのパス :rtype: str """ utils.check_arg({"text": text}) if file_name: file_name = utils.make_dir(file_name) _text = "{}\n".format(text) with file_name.open(mode="w", encoding="utf-8") as fd: fd.write(_text) self.logger.info(Msg.ml_exported, file_name) else: enco = utils.get_encoding() _text = text.encode(enco, utils.BACKSLASH).decode(enco) + "\n" print(_text) return _text
def export(self, list_id, file_name=None, survey=False): """ そのマイリストに登録された動画のIDを一覧する。 :param int | str list_id: マイリストの名前またはID。0で「とりあえずマイリスト」。 :param str | Path | None file_name: ファイル名。ここにリストを書き出す。 :param bool survey: Trueで全てのマイリストの情報をまとめて出力する。 :rtype: str """ utils.check_arg({"list_id": list_id, "survey": survey}) if file_name: file_name = utils.make_dir(file_name) if list_id == utils.ALL_ITEM: if survey: cont = self._construct_id(self.fetch_all(False)) else: cont = self._construct_id_name(self.fetch_meta(False)) else: cont = self._construct_id(self.fetch_one(list_id, False)) return self._writer(cont, file_name)
def get_thread_key(self, thread_id, needs_key): """ 専用のAPIにアクセスして thread_key を取得する。 :param str thread_id: :param str needs_key: :rtype: tuple[str, str] """ utils.check_arg(locals()) if not needs_key == "1": self.logger.debug("Video ID (or Thread ID): %s," " needs_key: %s", thread_id, needs_key) return "", "0" response = self.session.get(URL.URL_GetThreadKey, params={"thread": thread_id}) self.logger.debug("Response from GetThreadKey API: %s", response.text) parameters = parse_qs(response.text) threadkey = parameters["threadkey"][0] # type: str force_184 = parameters["force_184"][0] # type: str return threadkey, force_184
def _saver(self, video_id, comment_data, xml): """ :param str video_id: :param str comment_data: :param bool xml: :return: """ utils.check_arg(locals()) if xml and video_id.startswith(("sm", "nm")): extention = "xml" else: extention = "json" file_path = self.make_name(video_id, extention) self.logger.debug("File Path: %s", file_path) with file_path.open("w", encoding="utf-8") as f: f.write(comment_data + "\n") self.logger.info(Msg.nd_download_done, file_path) return True
def make_param_xml(self, thread_id, user_id): """ コメント取得用のxmlを構成する。 fork="1" があると投稿者コメントを取得する。 0-99999:9999,1000: 「0分~99999分までの範囲で 一分間あたり9999件、直近の1000件を取得する」の意味。 :param str thread_id: :param str user_id: :rtype: str """ utils.check_arg(locals()) self.logger.debug("Arguments: %s", locals()) return '<packet>' \ '<thread thread="{0}" user_id="{1}" version="20090904" scores="1"/>' \ '<thread thread="{0}" user_id="{1}" version="20090904" scores="1"' \ ' fork="1" res_from="-1000"/>' \ '<thread_leaves thread="{0}" user_id="{1}" scores="1">' \ '0-99999:9999,1000</thread_leaves>' \ '</packet>'.format(thread_id, user_id)
def fetch_all(self, with_info=True): """ 全てのマイリストに登録された動画情報を文字列にする。 :param bool with_info: :rtype: list[list[str]] """ utils.check_arg(locals()) container = [] if with_info: result_def = self.fetch_one(utils.DEFAULT_ID) container.extend(result_def) for l_id in self.mylists.keys(): result = self.fetch_one(l_id, False) container.extend(result) else: result_def = self.fetch_one(utils.DEFAULT_ID, False) container.extend(result_def) for l_id in self.mylists.keys(): result = self.fetch_one(l_id, False) container.extend([[item[0]] for item in result]) return container
def start(self, glossary, save_dir, xml=False): """ :param dict[str, dict[str, int | str]] | list[str] glossary: :param str | Path save_dir: :param bool xml: """ utils.check_arg(locals()) self.logger.debug("Directory to save in: %s", save_dir) self.logger.debug("Dictionary of Videos: %s", glossary) self.logger.debug("Download XML? : %s", xml) if isinstance(glossary, list): glossary = get_infos(glossary, self.logger) self.glossary = glossary self.save_dir = utils.make_dir(save_dir) self.logger.info(Msg.nd_start_dl_comment, len(self.glossary), list(self.glossary)) for index, video_id in enumerate(self.glossary.keys()): self.logger.info(Msg.nd_download_comment, index + 1, len(glossary), video_id, self.glossary[video_id][KeyGTI.TITLE]) self._download(video_id, xml) if len(self.glossary) > 1: time.sleep(1.5) return True
def _worker(self, video_id, url, is_large=True): """ サムネイル画像をダウンロードしにいく。 :param str video_id: 動画ID (e.g. sm1234) :param str url: 画像のURL :param bool is_large: 大きいサムネイルを取りに行くかどうか :rtype: bool | requests.Response """ utils.check_arg(locals()) db = self.glossary[video_id] with requests.Session() as session: try: # connect timeoutを5秒, read timeoutを10秒に設定 response = session.get(url=url, timeout=(5.0, 10.0)) if response.ok: return response # 大きいサムネイルを求めて404が返ってきたら標準の大きさで試す if response.status_code == 404: if is_large and url.endswith(".L"): return self._worker(video_id, url[:-2], is_large=False) else: self.logger.error(Err.connection_404, video_id, db[KeyGTI.TITLE]) return False except (TypeError, ConnectionError, socket.timeout, Timeout, urllib3.exceptions.TimeoutError, urllib3.exceptions.RequestError) as e: self.logger.debug("An exception occurred: %s", e) if is_large and url.endswith(".L"): return self._worker(video_id, url[:-2], is_large=False) else: self.logger.error( Err.connection_timeout.format(video_id) + " (タイトル: {})".format(db[KeyGTI.TITLE])) return False
def start(self, glossary, save_dir, is_large=True): """ :param dict[str, dict[str, int | str]] | list[str] glossary: :param str | Path save_dir: :param bool is_large: 大きいサムネイルを取りに行くかどうか :rtype: bool """ utils.check_arg(locals()) self.logger.debug("Directory to save in: %s", save_dir) self.logger.debug("Dictionary of Videos: %s", glossary) if isinstance(glossary, list): glossary = get_infos(glossary, self.logger) self.glossary = glossary self.save_dir = utils.make_dir(save_dir) self.logger.info(Msg.nd_start_dl_pict, len(self.glossary), list(self.glossary)) for index, video_id in enumerate(self.glossary.keys()): self.logger.info(Msg.nd_download_pict, index + 1, len(glossary), video_id, self.glossary[video_id][KeyGTI.TITLE]) self._download(video_id, is_large) return True
def make_param_json(self, official_video, user_id, user_key, thread_id, optional_thread_id=None, thread_key=None, force_184=None): """ コメント取得用のjsonを構成する。 fork="1" があると投稿者コメントを取得する。 0-99999:9999,1000: 「0分~99999分までの範囲で 一分間あたり9999件、直近の1000件を取得する」の意味。 :param bool official_video: 公式動画なら True :param str user_id: :param str user_key: :param str thread_id: :param str | None optional_thread_id: :param str | None thread_key: :param str | None force_184: :rtype: list[dict] """ utils.check_arg({ "official_video": official_video, "user_id": user_id, "user_key": user_key, "thread_id": thread_id }) self.logger.debug("Arguments of creating JSON: %s", locals()) result = [ { "ping": { "content": "rs:0" } }, { "ping": { "content": "ps:0" } }, { "thread": { "thread": optional_thread_id or thread_id, "version": "20090904", "language": 0, "user_id": user_id, "with_global": 1, "scores": 1, "nicoru": 0, "userkey": user_key } }, { "ping": { "content": "pf:0" } }, { "ping": { "content": "ps:1" } }, { "thread_leaves": { "thread": optional_thread_id or thread_id, "language": 0, "user_id": user_id, # "content" : "0-4:100,250", # 公式仕様のデフォルト値 "content": "0-99999:9999,1000", "scores": 1, "nicoru": 0, "userkey": user_key } }, { "ping": { "content": "pf:1" } } ] if official_video: result += [ { "ping": { "content": "ps:2" } }, { "thread": { "thread": thread_id, "version": "20090904", "language": 0, "user_id": user_id, "force_184": force_184, "with_global": 1, "scores": 1, "nicoru": 0, "threadkey": thread_key } }, { "ping": { "content": "pf:2" } }, { "ping": { "content": "ps:3" } }, { "thread_leaves": { "thread": thread_id, "language": 0, "user_id": user_id, # "content" : "0-4:100,250", # 公式仕様のデフォルト値 "content": "0-99999:9999,1000", "scores": 1, "nicoru": 0, "force_184": force_184, "threadkey": thread_key } }, { "ping": { "content": "pf:3" } } ] result += [{"ping": {"content": "rf:0"}}] return result
def get_response(self, mode, **kwargs): """ マイリストAPIにアクセスして結果を受け取る。 * bool is_def: (add, copy, move, delete) 「とりあえずマイリスト」が対象であれば True * bool is_public: (add) 公開マイリストであれば True * int list_id: (purge) マイリストのID * int list_id_to: (add, copy, move) マイリストのID * int list_id_from: (copy, move, delete) マイリストのID * str video_id: (add, copy, move, delete) 動画ID * str item_id: (add, copy, move, delete) 動画の item ID * str mylist_name: (create) マイリストの名前 * str description: (add, create) 動画またはマイリストの説明文 * int default_sort: (create) 並び順 * int icon_id: (create) マイリストのアイコンを表す数字 :param str mode: "add", "copy", "move", "delete", "purge", "create" のいずれか :rtype: dict """ utils.check_arg(locals()) assert mode.lower() in ("add", "delete", "copy", "move", "purge", "create") self.logger.debug("Query components: %s", kwargs) to_def = kwargs.get("to_def") # type: bool from_def = kwargs.get("from_def") # type: bool is_public = kwargs.get("is_public") # type: bool list_id = kwargs.get("list_id") # type: int list_id_to = kwargs.get("list_id_to") # type: int list_id_from = kwargs.get("list_id_from") # type: int video_id = kwargs.get("video_id") # type: str item_id = kwargs.get("item_id") # type: str mylist_name = kwargs.get("mylist_name") # type: str description = kwargs.get("description", "") # type: str default_sort = kwargs.get("default_sort", 0) # type: int icon_id = kwargs.get("icon_id", 0) # type: int if video_id and not isinstance(video_id, list): video_id = [video_id] if item_id and not isinstance(item_id, list): item_id = [item_id] if "move" == mode and to_def: # とりあえずマイリストには直接移動できないので、追加と削除を別でやる。 self.get_response("add", to_def=True, video_id=video_id, description=description) return self.get_response("delete", from_def=True, item_id=item_id) if "add" == mode or ("copy" == mode and to_def): payload = { "item_type": 0, "item_id": video_id, "description": description, "token": self.token } if to_def: url = URL.URL_AddDef else: payload.update({"group_id": str(list_id_to)}) url = URL.URL_AddItem elif "delete" == mode: payload = {"id_list[0][]": item_id, "token": self.token} if from_def: url = URL.URL_DeleteDef else: payload.update({"group_id": str(list_id_from)}) url = URL.URL_DeleteItem elif "copy" == mode: payload = { "target_group_id": str(list_id_to), "id_list[0][]": item_id, "token": self.token } if from_def: url = URL.URL_CopyDef else: payload.update({"group_id": str(list_id_from)}) url = URL.URL_CopyItem elif "move" == mode: payload = { "target_group_id": str(list_id_to), "id_list[0][]": item_id, "token": self.token } if from_def: url = URL.URL_MoveDef else: payload.update({"group_id": str(list_id_from)}) url = URL.URL_MoveItem elif "purge" == mode: payload = {"group_id": str(list_id), "token": self.token} url = URL.URL_PurgeList else: # create payload = { "name": mylist_name, "description": description, "public": int(is_public), "default_sort": default_sort, "icon_id": icon_id, "token": self.token } url = URL.URL_AddMyList self.logger.debug("URL: %s", url) self.logger.debug("Query to post: %s", payload) res = self.session.get(url, params=payload).text self.logger.debug("Response: %s", res) return json.loads(res)
def move(self, list_id_from, list_id_to, *videoids): """ そのマイリストに、 指定した動画を移動する。 :param int | str list_id_from: 移動元のIDまたは名前 :param int | str list_id_to: 移動先のIDまたは名前 :param str videoids: 動画ID :rtype: bool """ utils.check_arg(locals()) if len(videoids) > 1 and utils.ALL_ITEM in videoids: raise utils.MylistError(Err.videoids_contain_all) list_id_from, list_name_from = self._get_list_id(list_id_from) list_id_to, list_name_to = self._get_list_id(list_id_to) to_def = (list_id_to == utils.DEFAULT_ID) from_def = (list_id_from == utils.DEFAULT_ID) item_ids = self.get_item_ids(list_id_from, *videoids) if len(item_ids) == 0: self.logger.error(Err.no_items) return False if utils.ALL_ITEM not in videoids: item_ids = { vd_id: item_ids[vd_id] for vd_id in videoids if vd_id in item_ids } # 指定したものが含まれているかの確認 excluded = [vd_id for vd_id in videoids if vd_id not in item_ids] if len(excluded) > 0: self.logger.error(Err.item_not_contained, list_name_from, excluded) self.logger.info(Msg.ml_will_move, list_name_from, list_name_to, sorted(item_ids.keys())) _done = [] for _counter, vd_id in enumerate(item_ids): _counter += 1 if to_def: # とりあえずマイリストには直接移動できないので、追加と削除を別でやる。 res = self.get_response("add", to_def=True, video_id=vd_id, item_id=item_ids[vd_id]) try: self._should_continue(res, video_id=vd_id, list_name=list_name_to, count_now=_counter, count_whole=len(item_ids)) except MylistAPIError as error: if error.ok: return True else: # エラーが起きた場合 self.logger.error(Err.remaining, [ i for i in videoids if i not in _done and i != utils.ALL_ITEM ]) raise res = self.get_response("delete", from_def=True, video_id=vd_id, item_id=item_ids[vd_id]) else: res = self.get_response("move", item_id=item_ids[vd_id], from_def=from_def, list_id_to=list_id_to, list_id_from=list_id_from) try: self._should_continue(res, video_id=vd_id, list_name=list_name_to, count_now=_counter, count_whole=len(item_ids)) self.logger.info(Msg.ml_done_move, _counter, len(item_ids), vd_id) _done.append(vd_id) except MylistAPIError as error: if error.ok: return True else: # エラーが起きた場合 self.logger.error(Err.remaining, [ i for i in videoids if i not in _done and i != utils.ALL_ITEM ]) raise return True
def delete(self, list_id, *videoids, confident=False): """ そのマイリストから、指定した動画を削除する。 :param int | str list_id: 移動元のIDまたは名前 :param str videoids: 動画ID :param bool confident: :rtype: bool """ utils.check_arg(locals()) if len(videoids) > 1 and utils.ALL_ITEM in videoids: raise utils.MylistError(Err.videoids_contain_all) list_id, list_name = self._get_list_id(list_id) from_def = (list_id == utils.DEFAULT_ID) item_ids = self.get_item_ids(list_id, *videoids) if len(item_ids) == 0: self.logger.error(Err.no_items) return False if len(videoids) == 1 and utils.ALL_ITEM in videoids: # 全体モード if not confident and not self._confirmation( "delete", list_name, sorted(item_ids.keys())): print(Msg.ml_answer_no) return False self.logger.info(Msg.ml_will_delete, list_name, sorted(item_ids.keys())) else: # 個別モード self.logger.info(Msg.ml_will_delete, list_name, list(videoids)) item_ids = { vd_id: item_ids[vd_id] for vd_id in videoids if vd_id in item_ids } # 指定したIDが含まれているかの確認 excluded = [vd_id for vd_id in videoids if vd_id not in item_ids] if len(excluded) > 0: self.logger.error(Err.item_not_contained, list_name, excluded) _done = [] for _counter, vd_id in enumerate(item_ids): _counter += 1 res = self.get_response("delete", from_def=from_def, list_id_from=list_id, item_id=item_ids[vd_id]) try: self._should_continue(res, video_id=vd_id, list_name=list_name, count_now=_counter, count_whole=len(item_ids)) self.logger.info(Msg.ml_done_delete, _counter, len(item_ids), vd_id) _done.append(vd_id) except MylistAPIError as error: if error.ok: return True else: # エラーが起きた場合 self.logger.error(Err.remaining, [ i for i in videoids if i not in _done and i != utils.ALL_ITEM ]) raise return True
def fetch_one(self, list_id, with_header=True): """ 単一のマイリストに登録された動画情報を文字列にする。 deleted について: * 1 = 投稿者による削除 * 2 = 運営による削除 * 3 = 権利者による削除 * 8 = 投稿者による非公開 :param int | str list_id: マイリストの名前またはID。 :param bool with_header: :rtype: list[list[str]] """ utils.check_arg(locals()) list_id, list_name = self._get_list_id(list_id) self.logger.info(Msg.ml_showing_mylist, list_name) if list_id == utils.DEFAULT_ID: jtext = json.loads(self.session.get(URL.URL_ListDef).text) else: jtext = json.loads( self.session.get(URL.URL_ListOne, params={ "group_id": list_id }).text) self.logger.debug("Returned: %s", jtext) if with_header: container = [[ "動画 ID", "タイトル", "投稿日", "再生数", "コメント数", "マイリスト数", "長さ", "状態", "メモ", "所屬", # "最近のコメント", ]] else: container = [] for item in jtext["mylistitem"]: data = item[MKey.ITEM_DATA] desc = html.unescape(item[MKey.DESCRIPTION]) duration = int(data[KeyGTI.LENGTH_SECONDS]) container.append([ data[KeyGTI.VIDEO_ID], html.unescape(data[KeyGTI.TITLE]).replace(r"\/", "/"), self._get_jst_from_utime(data[KeyGTI.FIRST_RETRIEVE]), data[KeyGTI.VIEW_COUNTER], data[KeyGTI.NUM_RES], data[KeyGTI.MYLIST_COUNTER], "{}:{}".format(duration // 60, duration % 60), self.WHY_DELETED.get(data[KeyGTI.DELETED], "不明"), desc.strip().replace("\r", "").replace("\n", " ").replace(r"\/", "/"), list_name, # data[KeyGTI.LAST_RES_BODY], ]) self.logger.debug("Mylists info: %s", container) return container